diff --git a/app/src/androidTest/java/com/owncloud/android/files/services/FileUploaderIT.kt b/app/src/androidTest/java/com/owncloud/android/files/services/FileUploaderIT.kt
index e9f844454e3a..a58b7722532a 100644
--- a/app/src/androidTest/java/com/owncloud/android/files/services/FileUploaderIT.kt
+++ b/app/src/androidTest/java/com/owncloud/android/files/services/FileUploaderIT.kt
@@ -16,6 +16,7 @@ import com.nextcloud.client.jobs.upload.FileUploadHelper
import com.nextcloud.client.jobs.upload.FileUploadWorker
import com.nextcloud.client.network.Connectivity
import com.nextcloud.client.network.ConnectivityService
+import com.nextcloud.model.OCUploadLocalPathData
import com.owncloud.android.AbstractOnServerIT
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.datamodel.UploadsStorageManager
@@ -123,7 +124,7 @@ abstract class FileUploaderIT : AbstractOnServerIT() {
fun testKeepLocalAndOverwriteRemoteStatic() {
val file = getDummyFile("chunkedFile.txt")
- FileUploadHelper().uploadNewFiles(
+ val data = OCUploadLocalPathData(
user,
arrayOf(file.absolutePath),
arrayOf("/testFile.txt"),
@@ -135,6 +136,8 @@ abstract class FileUploaderIT : AbstractOnServerIT() {
NameCollisionPolicy.DEFAULT
)
+ FileUploadHelper().uploadNewFiles(data)
+
longSleep()
val result = ReadFileRemoteOperation("/testFile.txt").execute(client)
@@ -239,7 +242,7 @@ abstract class FileUploaderIT : AbstractOnServerIT() {
fun testKeepBothStatic() {
val file = getDummyFile("nonEmpty.txt")
- FileUploadHelper().uploadNewFiles(
+ val data = OCUploadLocalPathData(
user,
arrayOf(file.absolutePath),
arrayOf("/testFile.txt"),
@@ -250,6 +253,7 @@ abstract class FileUploaderIT : AbstractOnServerIT() {
false,
NameCollisionPolicy.DEFAULT
)
+ FileUploadHelper().uploadNewFiles(data)
longSleep()
@@ -347,7 +351,7 @@ abstract class FileUploaderIT : AbstractOnServerIT() {
fun testKeepServerStatic() {
val file = getDummyFile("chunkedFile.txt")
- FileUploadHelper().uploadNewFiles(
+ val data = OCUploadLocalPathData(
user,
arrayOf(file.absolutePath),
arrayOf("/testFile.txt"),
@@ -358,6 +362,7 @@ abstract class FileUploaderIT : AbstractOnServerIT() {
false,
NameCollisionPolicy.DEFAULT
)
+ FileUploadHelper().uploadNewFiles(data)
longSleep()
@@ -451,7 +456,7 @@ abstract class FileUploaderIT : AbstractOnServerIT() {
fun testKeepCancelStatic() {
val file = getDummyFile("chunkedFile.txt")
- FileUploadHelper().uploadNewFiles(
+ val data = OCUploadLocalPathData(
user,
arrayOf(file.absolutePath),
arrayOf("/testFile.txt"),
@@ -462,6 +467,7 @@ abstract class FileUploaderIT : AbstractOnServerIT() {
false,
NameCollisionPolicy.DEFAULT
)
+ FileUploadHelper().uploadNewFiles(data)
longSleep()
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ee6e7fd2bc80..74ab1f7108dc 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,7 +1,7 @@
@@ -636,6 +636,9 @@
android:launchMode="singleTop"
android:theme="@style/Theme.ownCloud.Dialog.NoTitle"
android:windowSoftInputMode="adjustResize" />
+
+ * SPDX-FileCopyrightText: 2024-2025 TSI-mc
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
@@ -32,6 +32,7 @@
import com.nextcloud.ui.ImageDetailFragment;
import com.nextcloud.ui.SetOnlineStatusBottomSheet;
import com.nextcloud.ui.SetStatusMessageBottomSheet;
+import com.nextcloud.ui.albumItemActions.AlbumItemActionsBottomSheet;
import com.nextcloud.ui.composeActivity.ComposeActivity;
import com.nextcloud.ui.fileactions.FileActionsBottomSheet;
import com.nextcloud.ui.trashbinFileActions.TrashbinFileActionsBottomSheet;
@@ -82,6 +83,7 @@
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.CreateAlbumDialogFragment;
import com.owncloud.android.ui.dialog.CreateFolderDialogFragment;
import com.owncloud.android.ui.dialog.ExpirationDatePickerDialogFragment;
import com.owncloud.android.ui.dialog.IndeterminateProgressDialog;
@@ -114,6 +116,9 @@
import com.owncloud.android.ui.fragment.OCFileListFragment;
import com.owncloud.android.ui.fragment.SharedListFragment;
import com.owncloud.android.ui.fragment.UnifiedSearchFragment;
+import com.owncloud.android.ui.fragment.albums.AlbumItemsFragment;
+import com.owncloud.android.ui.fragment.albums.AlbumsFragment;
+import com.owncloud.android.ui.activity.AlbumsPickerActivity;
import com.owncloud.android.ui.fragment.contactsbackup.BackupFragment;
import com.owncloud.android.ui.fragment.contactsbackup.BackupListFragment;
import com.owncloud.android.ui.preview.FileDownloadFragment;
@@ -505,4 +510,19 @@ abstract class ComponentsModule {
@ContributesAndroidInjector
abstract SetStatusMessageBottomSheet setStatusMessageBottomSheet();
+
+ @ContributesAndroidInjector
+ abstract AlbumsPickerActivity albumsPickerActivity();
+
+ @ContributesAndroidInjector
+ abstract CreateAlbumDialogFragment createAlbumDialogFragment();
+
+ @ContributesAndroidInjector
+ abstract AlbumsFragment albumsFragment();
+
+ @ContributesAndroidInjector
+ abstract AlbumItemsFragment albumItemsFragment();
+
+ @ContributesAndroidInjector
+ abstract AlbumItemActionsBottomSheet albumItemActionsBottomSheet();
}
diff --git a/app/src/main/java/com/nextcloud/client/documentscan/DocumentScanViewModel.kt b/app/src/main/java/com/nextcloud/client/documentscan/DocumentScanViewModel.kt
index e7cc433c2b89..b449b2c80cdb 100644
--- a/app/src/main/java/com/nextcloud/client/documentscan/DocumentScanViewModel.kt
+++ b/app/src/main/java/com/nextcloud/client/documentscan/DocumentScanViewModel.kt
@@ -18,6 +18,7 @@ import com.nextcloud.client.jobs.BackgroundJobManager
import com.nextcloud.client.jobs.upload.FileUploadHelper
import com.nextcloud.client.jobs.upload.FileUploadWorker
import com.nextcloud.client.logger.Logger
+import com.nextcloud.model.OCUploadLocalPathData
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.files.services.NameCollisionPolicy
import com.owncloud.android.operations.UploadFileOperation
@@ -165,17 +166,8 @@ class DocumentScanViewModel @Inject constructor(
uploadFolder + OCFile.PATH_SEPARATOR + File(it).name
}.toTypedArray()
- FileUploadHelper.instance().uploadNewFiles(
- currentAccountProvider.user,
- pageList.toTypedArray(),
- uploadPaths,
- FileUploadWorker.LOCAL_BEHAVIOUR_DELETE,
- true,
- UploadFileOperation.CREATED_BY_USER,
- false,
- false,
- NameCollisionPolicy.ASK_USER
- )
+ val data = OCUploadLocalPathData.forDocument(currentAccountProvider.user, pageList.toTypedArray(), uploadPaths)
+ FileUploadHelper.instance().uploadNewFiles(data)
}
fun onExportCanceled() {
diff --git a/app/src/main/java/com/nextcloud/client/documentscan/GeneratePdfFromImagesWork.kt b/app/src/main/java/com/nextcloud/client/documentscan/GeneratePdfFromImagesWork.kt
index c1115de62e35..f400bd5392b5 100644
--- a/app/src/main/java/com/nextcloud/client/documentscan/GeneratePdfFromImagesWork.kt
+++ b/app/src/main/java/com/nextcloud/client/documentscan/GeneratePdfFromImagesWork.kt
@@ -20,6 +20,7 @@ import com.nextcloud.client.account.UserAccountManager
import com.nextcloud.client.jobs.upload.FileUploadHelper
import com.nextcloud.client.jobs.upload.FileUploadWorker
import com.nextcloud.client.logger.Logger
+import com.nextcloud.model.OCUploadLocalPathData
import com.owncloud.android.R
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.files.services.NameCollisionPolicy
@@ -108,18 +109,8 @@ class GeneratePdfFromImagesWork(
private fun uploadFile(user: User, uploadFolder: String, pdfPath: String) {
val uploadPath = uploadFolder + OCFile.PATH_SEPARATOR + File(pdfPath).name
- FileUploadHelper().uploadNewFiles(
- user,
- arrayOf(pdfPath),
- arrayOf(uploadPath),
- // MIME type will be detected from file name
- FileUploadWorker.LOCAL_BEHAVIOUR_DELETE,
- true,
- UploadFileOperation.CREATED_BY_USER,
- false,
- false,
- NameCollisionPolicy.ASK_USER
- )
+ val data = OCUploadLocalPathData.forDocument(user, arrayOf(pdfPath), arrayOf(uploadPath))
+ FileUploadHelper().uploadNewFiles(data)
}
companion object {
diff --git a/app/src/main/java/com/nextcloud/client/editimage/EditImageActivity.kt b/app/src/main/java/com/nextcloud/client/editimage/EditImageActivity.kt
index 795f974d1eb3..d57e6ec412f0 100644
--- a/app/src/main/java/com/nextcloud/client/editimage/EditImageActivity.kt
+++ b/app/src/main/java/com/nextcloud/client/editimage/EditImageActivity.kt
@@ -22,6 +22,7 @@ import com.canhub.cropper.CropImageView
import com.nextcloud.client.di.Injectable
import com.nextcloud.client.jobs.upload.FileUploadHelper
import com.nextcloud.client.jobs.upload.FileUploadWorker
+import com.nextcloud.model.OCUploadLocalPathData
import com.nextcloud.utils.extensions.getParcelableArgument
import com.owncloud.android.R
import com.owncloud.android.databinding.ActivityEditImageBinding
@@ -95,17 +96,18 @@ class EditImageActivity :
resultUri?.substring(resultUri.lastIndexOf('.'))
resultUri?.let {
- FileUploadHelper().uploadNewFiles(
+ val data = OCUploadLocalPathData(
user = storageManager.user,
localPaths = arrayOf(it),
remotePaths = arrayOf(file.parentRemotePath + File.separator + newFileName),
createRemoteFolder = false,
- createdBy = UploadFileOperation.CREATED_BY_USER,
+ creationType = UploadFileOperation.CREATED_BY_USER,
requiresWifi = false,
requiresCharging = false,
- nameCollisionPolicy = NameCollisionPolicy.RENAME,
+ collisionPolicy = NameCollisionPolicy.RENAME,
localBehavior = FileUploadWorker.LOCAL_BEHAVIOUR_DELETE
)
+ FileUploadHelper().uploadNewFiles(data)
}
}
diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt
index 4b1dd1d659b2..44b7b9159db7 100644
--- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt
+++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt
@@ -2,6 +2,7 @@
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz
+ * SPDX-FileCopyrightText: 2026 TSI-mc
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.jobs
@@ -28,6 +29,7 @@ import com.nextcloud.client.jobs.download.FileDownloadWorker
import com.nextcloud.client.jobs.folderDownload.FolderDownloadWorker
import com.nextcloud.client.jobs.metadata.MetadataWorker
import com.nextcloud.client.jobs.offlineOperations.OfflineOperationsWorker
+import com.nextcloud.client.jobs.upload.AlbumFileUploadWorker
import com.nextcloud.client.jobs.upload.FileUploadWorker
import com.nextcloud.client.logger.Logger
import com.nextcloud.client.network.ConnectivityService
@@ -96,6 +98,7 @@ class BackgroundJobFactory @Inject constructor(
CalendarImportWork::class -> createCalendarImportWork(context, workerParameters)
FilesExportWork::class -> createFilesExportWork(context, workerParameters)
FileUploadWorker::class -> createFilesUploadWorker(context, workerParameters)
+ AlbumFileUploadWorker::class -> createAlbumsFilesUploadWorker(context, workerParameters)
FileDownloadWorker::class -> createFilesDownloadWorker(context, workerParameters)
GeneratePdfFromImagesWork::class -> createPDFGenerateWork(context, workerParameters)
HealthStatusWork::class -> createHealthStatusWork(context, workerParameters)
@@ -251,6 +254,20 @@ class BackgroundJobFactory @Inject constructor(
params
)
+ private fun createAlbumsFilesUploadWorker(context: Context, params: WorkerParameters): AlbumFileUploadWorker =
+ AlbumFileUploadWorker(
+ uploadsStorageManager,
+ connectivityService,
+ powerManagementService,
+ accountManager,
+ viewThemeUtils.get(),
+ localBroadcastManager.get(),
+ backgroundJobManager.get(),
+ preferences,
+ context,
+ params
+ )
+
private fun createPDFGenerateWork(context: Context, params: WorkerParameters): GeneratePdfFromImagesWork =
GeneratePdfFromImagesWork(
appContext = context,
diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt
index abde68ad4040..d166ec269bf0 100644
--- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt
+++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt
@@ -2,6 +2,7 @@
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz
+ * SPDX-FileCopyrightText: 2026 TSI-mc
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.jobs
@@ -138,6 +139,7 @@ interface BackgroundJobManager {
fun startNotificationJob(subject: String, signature: String)
fun startAccountRemovalJob(accountName: String, remoteWipe: Boolean)
fun startFilesUploadJob(user: User, uploadIds: LongArray, showSameFileAlreadyExistsNotification: Boolean)
+ fun startAlbumFilesUploadJob(user: User, uploadIds: LongArray, albumName: String)
fun getFileUploads(user: User): LiveData>
fun cancelFilesUploadJob(user: User)
fun isStartFileUploadJobScheduled(accountName: String): Boolean
diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt
index 0ad01e66c7ad..ff4569be1793 100644
--- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt
+++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt
@@ -2,6 +2,7 @@
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz
+ * SPDX-FileCopyrightText: 2026 TSI-mc
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.jobs
@@ -32,6 +33,7 @@ import com.nextcloud.client.jobs.download.FileDownloadWorker
import com.nextcloud.client.jobs.folderDownload.FolderDownloadWorker
import com.nextcloud.client.jobs.metadata.MetadataWorker
import com.nextcloud.client.jobs.offlineOperations.OfflineOperationsWorker
+import com.nextcloud.client.jobs.upload.AlbumFileUploadWorker
import com.nextcloud.client.jobs.upload.FileUploadHelper
import com.nextcloud.client.jobs.upload.FileUploadWorker
import com.nextcloud.client.preferences.AppPreferences
@@ -86,6 +88,7 @@ internal class BackgroundJobManagerImpl(
const val JOB_NOTIFICATION = "notification"
const val JOB_ACCOUNT_REMOVAL = "account_removal"
const val JOB_FILES_UPLOAD = "files_upload"
+ const val ALBUM_JOB_FILES_UPLOAD = "album_files_upload"
const val JOB_FOLDER_DOWNLOAD = "folder_download"
const val JOB_FILES_DOWNLOAD = "files_download"
const val JOB_PDF_GENERATION = "pdf_generation"
@@ -640,6 +643,8 @@ internal class BackgroundJobManagerImpl(
private fun startFileUploadJobTag(accountName: String): String = JOB_FILES_UPLOAD + accountName
+ private fun startAlbumsFileUploadJobTag(accountName: String): String = ALBUM_JOB_FILES_UPLOAD + accountName
+
override fun isStartFileUploadJobScheduled(accountName: String): Boolean =
workManager.isWorkScheduled(startFileUploadJobTag(accountName))
@@ -703,6 +708,59 @@ internal class BackgroundJobManagerImpl(
}
}
+ /**
+ * This method supports uploading and copying selected files to Album
+ *
+ * @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.
+ * @param albumName Album on which selected files should be copy after upload
+ */
+ override fun startAlbumFilesUploadJob(user: User, uploadIds: LongArray, albumName: String) {
+ defaultDispatcherScope.launch {
+ val batchSize = FileUploadHelper.MAX_FILE_COUNT
+ val batches = uploadIds.toList().chunked(batchSize)
+ val tag = startAlbumsFileUploadJobTag(user.accountName)
+
+ val constraints = Constraints.Builder()
+ .setRequiredNetworkType(NetworkType.CONNECTED)
+ .build()
+
+ val dataBuilder = Data.Builder()
+ .putString(AlbumFileUploadWorker.ACCOUNT, user.accountName)
+ .putInt(AlbumFileUploadWorker.TOTAL_UPLOAD_SIZE, uploadIds.size)
+ .putString(AlbumFileUploadWorker.ALBUM_NAME, albumName)
+
+ val workRequests = batches.mapIndexed { index, batch ->
+ dataBuilder
+ .putLongArray(AlbumFileUploadWorker.UPLOAD_IDS, batch.toLongArray())
+ .putInt(AlbumFileUploadWorker.CURRENT_BATCH_INDEX, index)
+
+ oneTimeRequestBuilder(AlbumFileUploadWorker::class, ALBUM_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()
+ }
+ }
+ }
+
private fun startFileDownloadJobTag(user: User, fileId: Long): String =
JOB_FOLDER_DOWNLOAD + user.accountName + fileId
diff --git a/app/src/main/java/com/nextcloud/client/jobs/notification/WorkerNotificationManager.kt b/app/src/main/java/com/nextcloud/client/jobs/notification/WorkerNotificationManager.kt
index 195a11b72a34..dac16b39f366 100644
--- a/app/src/main/java/com/nextcloud/client/jobs/notification/WorkerNotificationManager.kt
+++ b/app/src/main/java/com/nextcloud/client/jobs/notification/WorkerNotificationManager.kt
@@ -89,4 +89,15 @@ open class WorkerNotificationManager(
notification,
ForegroundServiceType.DataSync
)
+
+ fun createSilentNotification(title: String, iconId: Int): Notification = notificationBuilder
+ .setContentTitle(title)
+ .setSmallIcon(iconId)
+ .setOngoing(true)
+ .setSound(null)
+ .setVibrate(null)
+ .setOnlyAlertOnce(true)
+ .setPriority(NotificationCompat.PRIORITY_LOW)
+ .setSilent(true)
+ .build()
}
diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/AlbumFileUploadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/AlbumFileUploadWorker.kt
new file mode 100644
index 000000000000..48f527588c55
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/client/jobs/upload/AlbumFileUploadWorker.kt
@@ -0,0 +1,397 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 TSI-mc
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.nextcloud.client.jobs.upload
+
+import android.app.Notification
+import android.content.Context
+import androidx.localbroadcastmanager.content.LocalBroadcastManager
+import androidx.work.CoroutineWorker
+import androidx.work.ForegroundInfo
+import androidx.work.WorkerParameters
+import com.nextcloud.client.account.User
+import com.nextcloud.client.account.UserAccountManager
+import com.nextcloud.client.device.PowerManagementService
+import com.nextcloud.client.jobs.BackgroundJobManager
+import com.nextcloud.client.jobs.BackgroundJobManagerImpl
+import com.nextcloud.client.jobs.utils.UploadErrorNotificationManager
+import com.nextcloud.client.network.ConnectivityService
+import com.nextcloud.client.preferences.AppPreferences
+import com.nextcloud.utils.ForegroundServiceHelper
+import com.nextcloud.utils.extensions.getPercent
+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.ThumbnailsCacheManager
+import com.owncloud.android.datamodel.UploadsStorageManager
+import com.owncloud.android.db.OCUpload
+import com.owncloud.android.lib.common.OwnCloudAccount
+import com.owncloud.android.lib.common.OwnCloudClient
+import com.owncloud.android.lib.common.OwnCloudClientManagerFactory
+import com.owncloud.android.lib.common.network.OnDatatransferProgressListener
+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.albums.CopyFileToAlbumOperation
+import com.owncloud.android.utils.theme.ViewThemeUtils
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ensureActive
+import kotlinx.coroutines.withContext
+import java.io.File
+import kotlin.random.Random
+
+/**
+ * this worker is a replica of FileUploadWorker
+ * this worker will take care of upload and then copying the uploaded files to selected Album
+ */
+@Suppress("LongParameterList", "TooGenericExceptionCaught")
+class AlbumFileUploadWorker(
+ val uploadsStorageManager: UploadsStorageManager,
+ val connectivityService: ConnectivityService,
+ val powerManagementService: PowerManagementService,
+ val userAccountManager: UserAccountManager,
+ val viewThemeUtils: ViewThemeUtils,
+ val localBroadcastManager: LocalBroadcastManager,
+ private val backgroundJobManager: BackgroundJobManager,
+ val preferences: AppPreferences,
+ val context: Context,
+ params: WorkerParameters
+) : CoroutineWorker(context, params),
+ OnDatatransferProgressListener {
+
+ companion object {
+ val TAG: String = AlbumFileUploadWorker::class.java.simpleName
+
+ var currentUploadFileOperation: UploadFileOperation? = null
+
+ private const val BATCH_SIZE = 100
+
+ const val ALBUM_NAME = "album_name"
+
+ const val ACCOUNT = "data_account"
+ const val UPLOAD_IDS = "uploads_ids"
+ const val CURRENT_BATCH_INDEX = "batch_index"
+ const val TOTAL_UPLOAD_SIZE = "total_upload_size"
+ }
+
+ private var lastPercent = 0
+ private val notificationId = Random.nextInt()
+ private val notificationManager = UploadNotificationManager(context, viewThemeUtils, notificationId)
+ private val intents = FileUploaderIntents(context)
+ private val fileUploadBroadcastManager = FileUploadBroadcastManager(localBroadcastManager)
+
+ override suspend fun doWork(): Result = try {
+ Log_OC.d(TAG, "AlbumFileUploadWorker started")
+ val workerName = BackgroundJobManagerImpl.formatClassTag(this::class)
+ backgroundJobManager.logStartOfWorker(workerName)
+
+ trySetForeground()
+
+ val result = uploadFiles()
+ backgroundJobManager.logEndOfWorker(workerName, result)
+ notificationManager.dismissNotification()
+ result
+ } catch (t: Throwable) {
+ Log_OC.e(TAG, "exception $t")
+ currentUploadFileOperation?.cancel(null)
+ Result.failure()
+ } finally {
+ // Ensure all database operations are complete before signaling completion
+ uploadsStorageManager.notifyObserversNow()
+ notificationManager.dismissNotification()
+ }
+
+ private suspend fun trySetForeground() {
+ try {
+ val notificationTitle = notificationManager.currentOperationTitle
+ ?: context.getString(R.string.foreground_service_upload)
+
+ val notification = notificationManager.createSilentNotification(notificationTitle, R.drawable.uploads)
+ updateForegroundInfo(notification)
+ } catch (e: Exception) {
+ // Continue without foreground service - uploads will still work
+ Log_OC.w(TAG, "Could not set foreground service: ${e.message}")
+ }
+ }
+
+ override suspend fun getForegroundInfo(): ForegroundInfo {
+ val notificationTitle = notificationManager.currentOperationTitle
+ ?: context.getString(R.string.foreground_service_upload)
+ val notification = notificationManager.createSilentNotification(notificationTitle, R.drawable.uploads)
+
+ return ForegroundServiceHelper.createWorkerForegroundInfo(
+ notificationId,
+ notification,
+ ForegroundServiceType.DataSync
+ )
+ }
+
+ private suspend fun updateForegroundInfo(notification: Notification) {
+ val foregroundInfo = ForegroundServiceHelper.createWorkerForegroundInfo(
+ notificationId,
+ notification,
+ ForegroundServiceType.DataSync
+ )
+ setForeground(foregroundInfo)
+ }
+
+ @Suppress("ReturnCount", "LongMethod", "DEPRECATION")
+ private suspend fun uploadFiles(): Result = withContext(Dispatchers.IO) {
+ val accountName = inputData.getString(ACCOUNT)
+ if (accountName == null) {
+ Log_OC.e(TAG, "accountName is null")
+ return@withContext Result.failure()
+ }
+
+ val uploadIds = inputData.getLongArray(UPLOAD_IDS)
+ if (uploadIds == null) {
+ Log_OC.e(TAG, "uploadIds is null")
+ return@withContext Result.failure()
+ }
+
+ val currentBatchIndex = inputData.getInt(CURRENT_BATCH_INDEX, -1)
+ if (currentBatchIndex == -1) {
+ Log_OC.e(TAG, "currentBatchIndex is -1, cancelling")
+ return@withContext Result.failure()
+ }
+
+ val totalUploadSize = inputData.getInt(TOTAL_UPLOAD_SIZE, -1)
+ if (totalUploadSize == -1) {
+ Log_OC.e(TAG, "totalUploadSize is -1, cancelling")
+ return@withContext Result.failure()
+ }
+
+ // since worker's policy is append or replace and account name comes from there no need check in the loop
+ val optionalUser = userAccountManager.getUser(accountName)
+ if (!optionalUser.isPresent) {
+ Log_OC.e(TAG, "User not found for account: $accountName")
+ return@withContext Result.failure()
+ }
+
+ val albumName = inputData.getString(ALBUM_NAME)
+ if (albumName == null) {
+ Log_OC.e(TAG, "album name is null")
+ return@withContext Result.failure()
+ }
+
+ val user = optionalUser.get()
+ val previouslyUploadedFileSize = currentBatchIndex * FileUploadHelper.MAX_FILE_COUNT
+ val uploads = uploadsStorageManager.getUploadsByIds(uploadIds, accountName)
+ val ocAccount = OwnCloudAccount(user.toPlatformAccount(), context)
+ val client = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(ocAccount, context)
+
+ for ((index, upload) in uploads.withIndex()) {
+ ensureActive()
+
+ if (preferences.isGlobalUploadPaused) {
+ Log_OC.d(TAG, "Upload is paused, skip uploading files!")
+ notificationManager.notifyPaused(
+ intents.openUploadListIntent(null)
+ )
+ return@withContext Result.success()
+ }
+
+ if (canExitEarly()) {
+ notificationManager.showConnectionErrorNotification()
+ return@withContext Result.failure()
+ }
+
+ fileUploadBroadcastManager.sendAdded(context)
+ val operation = createUploadFileOperation(upload, user)
+ currentUploadFileOperation = operation
+
+ val currentIndex = (index + 1)
+ val currentUploadIndex = (currentIndex + previouslyUploadedFileSize)
+ notificationManager.prepareForStart(
+ operation,
+ startIntent = intents.openUploadListIntent(operation),
+ currentUploadIndex = currentUploadIndex,
+ totalUploadSize = totalUploadSize
+ )
+
+ val result = withContext(Dispatchers.IO) {
+ upload(operation, albumName, user, client)
+ }
+ val entity = uploadsStorageManager.uploadDao.getUploadById(upload.uploadId, accountName)
+ uploadsStorageManager.updateStatus(entity, result.isSuccess)
+ currentUploadFileOperation = null
+
+ if (result.code == ResultCode.QUOTA_EXCEEDED) {
+ Log_OC.w(TAG, "Quota exceeded, stopping uploads")
+ notificationManager.showQuotaExceedNotification(operation)
+ break
+ }
+
+ sendUploadFinishEvent(totalUploadSize, currentUploadIndex, operation, result)
+ }
+
+ return@withContext Result.success()
+ }
+
+ private fun sendUploadFinishEvent(
+ totalUploadSize: Int,
+ currentUploadIndex: Int,
+ operation: UploadFileOperation,
+ result: RemoteOperationResult<*>
+ ) {
+ val isLastUpload = currentUploadIndex == totalUploadSize
+
+ val shouldBroadcast =
+ (currentUploadIndex % BATCH_SIZE == 0 && totalUploadSize > BATCH_SIZE) ||
+ isLastUpload
+
+ if (shouldBroadcast) {
+ fileUploadBroadcastManager.sendFinished(
+ operation,
+ result,
+ operation.oldFile?.storagePath,
+ context
+ )
+ }
+ }
+
+ private fun canExitEarly(): Boolean {
+ val result = !connectivityService.isConnected ||
+ connectivityService.isInternetWalled ||
+ isStopped
+
+ if (result) {
+ Log_OC.d(TAG, "No internet connection, stopping worker.")
+ } else {
+ notificationManager.dismissErrorNotification()
+ }
+
+ 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@AlbumFileUploadWorker)
+ }
+
+ @Suppress("TooGenericExceptionCaught", "DEPRECATION")
+ private suspend fun upload(
+ operation: UploadFileOperation,
+ albumName: String,
+ user: User,
+ client: OwnCloudClient
+ ): RemoteOperationResult = withContext(Dispatchers.IO) {
+ lateinit var result: RemoteOperationResult
+
+ try {
+ val storageManager = operation.storageManager
+ result = operation.execute(client)
+ val task = ThumbnailsCacheManager.ThumbnailGenerationTask(storageManager, user)
+ val file = File(operation.originalStoragePath)
+ val remoteId: String? = operation.file.remoteId
+ task.execute(ThumbnailsCacheManager.ThumbnailGenerationTaskObject(file, remoteId))
+ val copyAlbumFileOperation =
+ CopyFileToAlbumOperation(operation.remotePath, albumName, storageManager)
+ val copyResult = copyAlbumFileOperation.execute(client)
+ if (copyResult.isSuccess) {
+ Log_OC.e(TAG, "Successful copied file to Album: $albumName")
+ } else {
+ Log_OC.e(TAG, "Failed to copy file to Album: $albumName due to ${copyResult.logMessage}")
+ }
+ fileUploadBroadcastManager.sendStarted(operation, context)
+ } catch (e: Exception) {
+ Log_OC.e(TAG, "Error uploading", e)
+ result = RemoteOperationResult(e)
+ } finally {
+ if (!isStopped) {
+ uploadsStorageManager.updateDatabaseUploadResult(result, operation)
+ // resolving file conflict will trigger normal file upload and shows two upload process
+ // one for normal and one for Album upload
+ // as customizing conflict can break normal upload
+ // so we are removing the upload if it's a conflict
+ // Note: this is fallback logic because default policy while uploading is RENAME
+ // if in some case code reach here it will remove the upload
+ // so we are checking it first and removing the upload
+ if (result.code == ResultCode.SYNC_CONFLICT) {
+ uploadsStorageManager.removeUpload(
+ operation.user.accountName,
+ operation.remotePath
+ )
+ } else {
+ UploadErrorNotificationManager.handleResult(
+ context,
+ notificationManager,
+ operation,
+ result,
+ onSameFileConflict = {
+ withContext(Dispatchers.Main) {
+ notificationManager.showSameFileAlreadyExistsNotification(operation.fileName)
+ }
+ }
+ )
+ }
+ }
+ }
+
+ return@withContext result
+ }
+
+ @Suppress("MagicNumber")
+ private val minProgressUpdateInterval = 750
+ private var lastUpdateTime = 0L
+
+ /**
+ * Receives from [com.owncloud.android.operations.UploadFileOperation.normalUpload]
+ */
+ @Suppress("MagicNumber")
+ override fun onTransferProgress(
+ progressRate: Long,
+ totalTransferredSoFar: Long,
+ totalToTransfer: Long,
+ fileAbsoluteName: String
+ ) {
+ val percent = getPercent(totalTransferredSoFar, totalToTransfer)
+ val currentTime = System.currentTimeMillis()
+
+ if (percent != lastPercent && (currentTime - lastUpdateTime) >= minProgressUpdateInterval) {
+ notificationManager.run {
+ val accountName = currentUploadFileOperation?.user?.accountName
+ val remotePath = currentUploadFileOperation?.remotePath
+
+ updateUploadProgress(percent, currentUploadFileOperation)
+
+ if (accountName != null && remotePath != null) {
+ val key: String = FileUploadHelper.buildRemoteName(accountName, remotePath)
+ val boundListener = FileUploadHelper.mBoundListeners[key]
+ val filename = currentUploadFileOperation?.fileName ?: ""
+
+ boundListener?.onTransferProgress(
+ progressRate,
+ totalTransferredSoFar,
+ totalToTransfer,
+ filename
+ )
+ }
+
+ dismissOldErrorNotification(currentUploadFileOperation)
+ }
+ lastUpdateTime = currentTime
+ }
+
+ lastPercent = percent
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt
index 809d24c8e66d..0fca75480fbd 100644
--- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt
+++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt
@@ -3,6 +3,7 @@
*
* SPDX-FileCopyrightText: 2023 Alper Ozturk
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
+ * SPDX-FileCopyrightText: 2026 TSI-mc
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.client.jobs.upload
@@ -22,6 +23,7 @@ import com.nextcloud.client.jobs.upload.FileUploadWorker.Companion.currentUpload
import com.nextcloud.client.notifications.AppWideNotificationManager
import com.nextcloud.client.network.Connectivity
import com.nextcloud.client.network.ConnectivityService
+import com.nextcloud.model.OCUploadLocalPathData
import com.nextcloud.utils.extensions.getUploadIds
import com.owncloud.android.MainApp
import com.owncloud.android.R
@@ -229,35 +231,33 @@ class FileUploadHelper {
}
@JvmOverloads
- @Suppress("LongParameterList")
- fun uploadNewFiles(
- user: User,
- localPaths: Array,
- remotePaths: Array,
- localBehavior: Int,
- createRemoteFolder: Boolean,
- createdBy: Int,
- requiresWifi: Boolean,
- requiresCharging: Boolean,
- nameCollisionPolicy: NameCollisionPolicy,
- showSameFileAlreadyExistsNotification: Boolean = true
- ) {
- val uploads = localPaths.mapIndexed { index, localPath ->
- val result = OCUpload(localPath, remotePaths[index], user.accountName).apply {
- this.nameCollisionPolicy = nameCollisionPolicy
- isUseWifiOnly = requiresWifi
- isWhileChargingOnly = requiresCharging
- uploadStatus = UploadStatus.UPLOAD_IN_PROGRESS
- this.createdBy = createdBy
- isCreateRemoteFolder = createRemoteFolder
- localAction = localBehavior
- }
+ fun uploadNewFiles(data: OCUploadLocalPathData, showSameFileAlreadyExistsNotification: Boolean = true) {
+ val uploads = getUploadsFromLocalPaths(data)
+ backgroundJobManager.startFilesUploadJob(
+ data.user,
+ uploads.getUploadIds(),
+ showSameFileAlreadyExistsNotification
+ )
+ }
- val id = uploadsStorageManager.uploadDao.insertOrReplace(result.toUploadEntity())
- result.uploadId = id
- result
- }
- backgroundJobManager.startFilesUploadJob(user, uploads.getUploadIds(), showSameFileAlreadyExistsNotification)
+ private fun getUploadsFromLocalPaths(data: OCUploadLocalPathData): List = data.localPaths.mapIndexed {
+ index,
+ localPath
+ ->
+ val result = data.toOCUpload(localPath, index)
+ val id = uploadsStorageManager.uploadDao.insertOrReplace(result.toUploadEntity())
+ result.uploadId = id
+ result
+ }
+
+ @Suppress("LongParameterList")
+ fun uploadAndCopyNewFilesForAlbum(data: OCUploadLocalPathData, albumName: String) {
+ val uploads = getUploadsFromLocalPaths(data)
+ backgroundJobManager.startAlbumFilesUploadJob(
+ data.user,
+ uploads.getUploadIds(),
+ albumName
+ )
}
fun removeFileUpload(remotePath: String, accountName: String) {
@@ -371,7 +371,7 @@ class FileUploadHelper {
@Suppress("ReturnCount")
fun isUploadingNow(upload: OCUpload?): Boolean {
- val currentUploadFileOperation = currentUploadFileOperation
+ val currentUploadFileOperation = currentUploadFileOperation ?: AlbumFileUploadWorker.currentUploadFileOperation
if (currentUploadFileOperation == null || currentUploadFileOperation.user == null) return false
if (upload == null || upload.accountName != currentUploadFileOperation.user.accountName) return false
diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt
index ea0bac258db8..ff7890242c33 100644
--- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt
+++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt
@@ -9,7 +9,6 @@ package com.nextcloud.client.jobs.upload
import android.app.Notification
import android.content.Context
-import androidx.core.app.NotificationCompat
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
@@ -39,7 +38,6 @@ 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.ui.notifications.NotificationUtils
import com.owncloud.android.utils.theme.ViewThemeUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ensureActive
@@ -146,7 +144,8 @@ class FileUploadWorker(
try {
val notificationTitle = notificationManager.currentOperationTitle
?: context.getString(R.string.foreground_service_upload)
- val notification = createNotification(notificationTitle)
+
+ val notification = notificationManager.createSilentNotification(notificationTitle, R.drawable.uploads)
updateForegroundInfo(notification)
} catch (e: Exception) {
// Continue without foreground service - uploads will still work
@@ -157,8 +156,7 @@ class FileUploadWorker(
override suspend fun getForegroundInfo(): ForegroundInfo {
val notificationTitle = notificationManager.currentOperationTitle
?: context.getString(R.string.foreground_service_upload)
- val notification = createNotification(notificationTitle)
-
+ val notification = notificationManager.createSilentNotification(notificationTitle, R.drawable.uploads)
return ForegroundServiceHelper.createWorkerForegroundInfo(
notificationId,
notification,
@@ -175,18 +173,6 @@ class FileUploadWorker(
setForeground(foregroundInfo)
}
- private fun createNotification(title: String): Notification =
- NotificationCompat.Builder(context, NotificationUtils.NOTIFICATION_CHANNEL_UPLOAD)
- .setContentTitle(title)
- .setSmallIcon(R.drawable.uploads)
- .setOngoing(true)
- .setSound(null)
- .setVibrate(null)
- .setOnlyAlertOnce(true)
- .setPriority(NotificationCompat.PRIORITY_LOW)
- .setSilent(true)
- .build()
-
@Suppress("ReturnCount", "LongMethod", "DEPRECATION")
private suspend fun uploadFiles(): Result = withContext(Dispatchers.IO) {
val accountName = inputData.getString(ACCOUNT)
diff --git a/app/src/main/java/com/nextcloud/model/OCUploadLocalPathData.kt b/app/src/main/java/com/nextcloud/model/OCUploadLocalPathData.kt
new file mode 100644
index 000000000000..4e7f4f8e133e
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/model/OCUploadLocalPathData.kt
@@ -0,0 +1,121 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.nextcloud.model
+
+import com.nextcloud.client.account.User
+import com.nextcloud.client.jobs.upload.FileUploadWorker
+import com.owncloud.android.datamodel.UploadsStorageManager.UploadStatus
+import com.owncloud.android.db.OCUpload
+import com.owncloud.android.files.services.NameCollisionPolicy
+import com.owncloud.android.operations.UploadFileOperation
+
+data class OCUploadLocalPathData(
+ val user: User,
+ val localPaths: Array,
+ val remotePaths: Array,
+ val localBehavior: Int,
+ val createRemoteFolder: Boolean,
+ val creationType: Int,
+ val requiresWifi: Boolean,
+ val requiresCharging: Boolean,
+ val collisionPolicy: NameCollisionPolicy
+) {
+ companion object {
+ fun forDocument(user: User, localPaths: Array, remotePaths: Array): OCUploadLocalPathData =
+ OCUploadLocalPathData(
+ user,
+ localPaths,
+ remotePaths,
+ FileUploadWorker.LOCAL_BEHAVIOUR_DELETE,
+ createRemoteFolder = true,
+ UploadFileOperation.CREATED_BY_USER,
+ requiresWifi = false,
+ requiresCharging = false,
+ NameCollisionPolicy.ASK_USER
+ )
+
+ fun forAlbum(
+ user: User,
+ localPaths: Array,
+ remotePaths: Array,
+ localBehavior: Int
+ ): OCUploadLocalPathData = OCUploadLocalPathData(
+ user,
+ localPaths,
+ remotePaths,
+ localBehavior,
+ createRemoteFolder = true,
+ UploadFileOperation.CREATED_BY_USER,
+ requiresWifi = false,
+ requiresCharging = false,
+ NameCollisionPolicy.RENAME
+ )
+
+ @JvmOverloads
+ fun forFile(
+ user: User,
+ localPaths: Array,
+ remotePaths: Array,
+ localBehavior: Int,
+ createRemoteFolder: Boolean = false
+ ): OCUploadLocalPathData = OCUploadLocalPathData(
+ user,
+ localPaths,
+ remotePaths,
+ localBehavior,
+ createRemoteFolder = createRemoteFolder,
+ UploadFileOperation.CREATED_BY_USER,
+ requiresWifi = false,
+ requiresCharging = false,
+ NameCollisionPolicy.ASK_USER
+ )
+ }
+
+ fun toOCUpload(localPath: String, index: Int): OCUpload =
+ OCUpload(localPath, remotePaths[index], user.accountName).apply {
+ nameCollisionPolicy = collisionPolicy
+ isUseWifiOnly = requiresWifi
+ isWhileChargingOnly = requiresCharging
+ uploadStatus = UploadStatus.UPLOAD_IN_PROGRESS
+ createdBy = creationType
+ isCreateRemoteFolder = createRemoteFolder
+ localAction = localBehavior
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as OCUploadLocalPathData
+
+ if (localBehavior != other.localBehavior) return false
+ if (createRemoteFolder != other.createRemoteFolder) return false
+ if (creationType != other.creationType) return false
+ if (requiresWifi != other.requiresWifi) return false
+ if (requiresCharging != other.requiresCharging) return false
+ if (user != other.user) return false
+ if (!localPaths.contentEquals(other.localPaths)) return false
+ if (!remotePaths.contentEquals(other.remotePaths)) return false
+ if (collisionPolicy != other.collisionPolicy) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = localBehavior
+ result = 31 * result + createRemoteFolder.hashCode()
+ result = 31 * result + creationType
+ result = 31 * result + requiresWifi.hashCode()
+ result = 31 * result + requiresCharging.hashCode()
+ result = 31 * result + user.hashCode()
+ result = 31 * result + localPaths.contentHashCode()
+ result = 31 * result + remotePaths.contentHashCode()
+ result = 31 * result + collisionPolicy.hashCode()
+ return result
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/ui/albumItemActions/AlbumItemAction.kt b/app/src/main/java/com/nextcloud/ui/albumItemActions/AlbumItemAction.kt
new file mode 100644
index 000000000000..9d31afca3def
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/ui/albumItemActions/AlbumItemAction.kt
@@ -0,0 +1,29 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2025 TSI-mc
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.nextcloud.ui.albumItemActions
+
+import androidx.annotation.DrawableRes
+import androidx.annotation.IdRes
+import androidx.annotation.StringRes
+import com.owncloud.android.R
+
+enum class AlbumItemAction(@IdRes val id: Int, @StringRes val title: Int, @DrawableRes val icon: Int? = null) {
+ RENAME_ALBUM(R.id.action_rename_file, R.string.album_rename, R.drawable.ic_edit),
+ DELETE_ALBUM(R.id.action_delete, R.string.album_delete, R.drawable.ic_delete);
+
+ companion object {
+ /**
+ * All file actions, in the order they should be displayed
+ */
+ @JvmField
+ val SORTED_VALUES = listOf(
+ RENAME_ALBUM,
+ DELETE_ALBUM
+ )
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/ui/albumItemActions/AlbumItemActionsBottomSheet.kt b/app/src/main/java/com/nextcloud/ui/albumItemActions/AlbumItemActionsBottomSheet.kt
new file mode 100644
index 000000000000..7c3c7e6d7572
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/ui/albumItemActions/AlbumItemActionsBottomSheet.kt
@@ -0,0 +1,127 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2025 TSI-mc
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.nextcloud.ui.albumItemActions
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.IdRes
+import androidx.appcompat.content.res.AppCompatResources
+import androidx.core.os.bundleOf
+import androidx.core.view.isEmpty
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.setFragmentResult
+import androidx.lifecycle.LifecycleOwner
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import com.nextcloud.android.common.ui.theme.utils.ColorRole
+import com.nextcloud.client.di.Injectable
+import com.owncloud.android.databinding.FileActionsBottomSheetBinding
+import com.owncloud.android.databinding.FileActionsBottomSheetItemBinding
+import com.owncloud.android.utils.theme.ViewThemeUtils
+import javax.inject.Inject
+
+class AlbumItemActionsBottomSheet :
+ BottomSheetDialogFragment(),
+ Injectable {
+
+ @Inject
+ lateinit var viewThemeUtils: ViewThemeUtils
+
+ private var _binding: FileActionsBottomSheetBinding? = null
+ val binding
+ get() = _binding!!
+
+ fun interface ResultListener {
+ fun onResult(@IdRes actionId: Int)
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ _binding = FileActionsBottomSheetBinding.inflate(inflater, container, false)
+
+ val bottomSheetDialog = dialog as BottomSheetDialog
+ bottomSheetDialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED
+ bottomSheetDialog.behavior.skipCollapsed = true
+
+ viewThemeUtils.platform.colorViewBackground(binding.bottomSheet, ColorRole.SURFACE)
+
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ binding.bottomSheetHeader.visibility = View.GONE
+ binding.bottomSheetLoading.visibility = View.GONE
+ displayActions()
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+
+ fun setResultListener(
+ fragmentManager: FragmentManager,
+ lifecycleOwner: LifecycleOwner,
+ listener: ResultListener
+ ): AlbumItemActionsBottomSheet {
+ fragmentManager.setFragmentResultListener(REQUEST_KEY, lifecycleOwner) { _, result ->
+ @IdRes val actionId = result.getInt(RESULT_KEY_ACTION_ID, -1)
+ if (actionId != -1) {
+ listener.onResult(actionId)
+ }
+ }
+ return this
+ }
+
+ private fun displayActions() {
+ if (binding.fileActionsList.isEmpty()) {
+ AlbumItemAction.SORTED_VALUES.forEach { action ->
+ val view = inflateActionView(action)
+ binding.fileActionsList.addView(view)
+ }
+ }
+ }
+
+ private fun inflateActionView(action: AlbumItemAction): View {
+ val itemBinding = FileActionsBottomSheetItemBinding.inflate(layoutInflater, binding.fileActionsList, false)
+ .apply {
+ root.setOnClickListener {
+ dispatchActionClick(action.id)
+ }
+ text.setText(action.title)
+ if (action.icon != null) {
+ val drawable =
+ viewThemeUtils.platform.tintDrawable(
+ requireContext(),
+ AppCompatResources.getDrawable(requireContext(), action.icon)!!
+ )
+ icon.setImageDrawable(drawable)
+ }
+ }
+ return itemBinding.root
+ }
+
+ private fun dispatchActionClick(id: Int?) {
+ if (id != null) {
+ setFragmentResult(REQUEST_KEY, bundleOf(RESULT_KEY_ACTION_ID to id))
+ parentFragmentManager.clearFragmentResultListener(REQUEST_KEY)
+ dismiss()
+ }
+ }
+
+ companion object {
+ private const val REQUEST_KEY = "REQUEST_KEY_ACTION"
+ private const val RESULT_KEY_ACTION_ID = "RESULT_KEY_ACTION_ID"
+
+ @JvmStatic
+ fun newInstance(): AlbumItemActionsBottomSheet = AlbumItemActionsBottomSheet()
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/ui/fileactions/FileActionsBottomSheet.kt b/app/src/main/java/com/nextcloud/ui/fileactions/FileActionsBottomSheet.kt
index 62467f7d9b0f..61930d097738 100644
--- a/app/src/main/java/com/nextcloud/ui/fileactions/FileActionsBottomSheet.kt
+++ b/app/src/main/java/com/nextcloud/ui/fileactions/FileActionsBottomSheet.kt
@@ -152,7 +152,8 @@ class FileActionsBottomSheet :
binding.thumbnailLayout.thumbnailShimmer,
syncedFolderProvider.preferences,
viewThemeUtils,
- syncedFolderProvider
+ syncedFolderProvider,
+ false
)
}
}
diff --git a/app/src/main/java/com/nextcloud/ui/trashbinFileActions/TrashbinFileActionsBottomSheet.kt b/app/src/main/java/com/nextcloud/ui/trashbinFileActions/TrashbinFileActionsBottomSheet.kt
index 3ba288a2970d..71de3d866c8c 100644
--- a/app/src/main/java/com/nextcloud/ui/trashbinFileActions/TrashbinFileActionsBottomSheet.kt
+++ b/app/src/main/java/com/nextcloud/ui/trashbinFileActions/TrashbinFileActionsBottomSheet.kt
@@ -128,7 +128,8 @@ class TrashbinFileActionsBottomSheet :
binding.thumbnailLayout.thumbnailShimmer,
syncedFolderProvider.preferences,
viewThemeUtils,
- syncedFolderProvider
+ syncedFolderProvider,
+ false
)
}
}
diff --git a/app/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java b/app/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java
index 2213a77b7439..ef5e53c8d12d 100644
--- a/app/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java
+++ b/app/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java
@@ -7,6 +7,7 @@
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz
* SPDX-FileCopyrightText: 2015 ownCloud Inc.
* SPDX-FileCopyrightText: 2014 David A. Velasco
+ * SPDX-FileCopyrightText: 2026 TSI-mc
* SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only)
*/
package com.owncloud.android.datamodel;
@@ -434,6 +435,7 @@ public static class ThumbnailGenerationTask extends AsyncTask asyncTasks,
boolean gridViewEnabled,
- String imageKey)
+ String imageKey,
+ boolean hideVideoOverlay)
throws IllegalArgumentException {
this(imageView, storageManager, user, asyncTasks);
this.gridViewEnabled = gridViewEnabled;
mImageKey = imageKey;
+ this.hideVideoOverlay = hideVideoOverlay;
}
public GetMethod getGetMethod() {
@@ -505,7 +509,7 @@ protected Bitmap doInBackground(ThumbnailGenerationTaskObject... params) {
if (mFile instanceof ServerFileInterface) {
thumbnail = doThumbnailFromOCFileInBackground();
- if (MimeTypeUtil.isVideo((ServerFileInterface) mFile) && thumbnail != null) {
+ if (MimeTypeUtil.isVideo((ServerFileInterface) mFile) && thumbnail != null && !hideVideoOverlay) {
thumbnail = addVideoOverlay(thumbnail, MainApp.getAppContext());
}
} else if (mFile instanceof File) {
@@ -514,7 +518,7 @@ protected Bitmap doInBackground(ThumbnailGenerationTaskObject... params) {
String url = ((File) mFile).getAbsolutePath();
String mMimeType = FileStorageUtils.getMimeTypeFromName(url);
- if (MimeTypeUtil.isVideo(mMimeType) && thumbnail != null) {
+ if (MimeTypeUtil.isVideo(mMimeType) && thumbnail != null && !hideVideoOverlay) {
thumbnail = addVideoOverlay(thumbnail, MainApp.getAppContext());
}
//} else { do nothing
diff --git a/app/src/main/java/com/owncloud/android/datamodel/VirtualFolderType.java b/app/src/main/java/com/owncloud/android/datamodel/VirtualFolderType.java
index 1259b706a2b1..e1fa9db746ff 100644
--- a/app/src/main/java/com/owncloud/android/datamodel/VirtualFolderType.java
+++ b/app/src/main/java/com/owncloud/android/datamodel/VirtualFolderType.java
@@ -1,6 +1,7 @@
/*
* Nextcloud - Android Client
*
+ * SPDX-FileCopyrightText: 2025 TSI-mc
* SPDX-FileCopyrightText: 2017 Tobias Kaminsky
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
@@ -12,5 +13,5 @@
* Type for virtual folders
*/
public enum VirtualFolderType {
- FAVORITE, GALLERY, NONE
+ FAVORITE, GALLERY, ALBUM, NONE
}
diff --git a/app/src/main/java/com/owncloud/android/operations/FetchRemoteFileOperation.kt b/app/src/main/java/com/owncloud/android/operations/FetchRemoteFileOperation.kt
new file mode 100644
index 000000000000..e4bd58a55764
--- /dev/null
+++ b/app/src/main/java/com/owncloud/android/operations/FetchRemoteFileOperation.kt
@@ -0,0 +1,112 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 TSI-mc
+ * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
+ */
+package com.owncloud.android.operations
+
+import android.content.Context
+import com.nextcloud.client.account.User
+import com.owncloud.android.MainApp
+import com.owncloud.android.datamodel.FileDataStorageManager
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.lib.common.OwnCloudClient
+import com.owncloud.android.lib.common.operations.RemoteOperation
+import com.owncloud.android.lib.common.operations.RemoteOperationResult
+import com.owncloud.android.lib.common.utils.Log_OC
+import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation
+import com.owncloud.android.lib.resources.files.SearchRemoteOperation
+import com.owncloud.android.lib.resources.files.model.RemoteFile
+import com.owncloud.android.operations.common.SyncOperation
+import com.owncloud.android.utils.FileStorageUtils
+
+/**
+ * fetch OCFile meta data if not present in local db
+ *
+ * @see com.owncloud.android.ui.asynctasks.FetchRemoteFileTask reference for this operation
+ *
+ * @param ocFile file for which metadata has to retrieve
+ * @param removeFileFromDb if you want to remove ocFile from local db to avoid duplicate entries for same fileId
+ */
+class FetchRemoteFileOperation(
+ private val context: Context,
+ private val user: User,
+ private val ocFile: OCFile,
+ private val removeFileFromDb: Boolean = false,
+ storageManager: FileDataStorageManager
+) : SyncOperation(storageManager) {
+
+ @Deprecated("Deprecated in Java")
+ @Suppress("ReturnCount")
+ override fun run(client: OwnCloudClient?): RemoteOperationResult<*>? {
+ val searchRemoteOperation = SearchRemoteOperation(
+ ocFile.localId.toString(),
+ SearchRemoteOperation.SearchType.FILE_ID_SEARCH,
+ false,
+ storageManager.getCapability(user)
+ )
+ val remoteOperationResult: RemoteOperationResult> =
+ searchRemoteOperation.execute(user, context)
+
+ if (remoteOperationResult.isSuccess && remoteOperationResult.resultData != null) {
+ if (remoteOperationResult.resultData.isEmpty()) {
+ Log_OC.e(TAG, "No remote file found with id: ${ocFile.localId}.")
+ return remoteOperationResult
+ }
+ val remotePath = (remoteOperationResult.resultData[0]).remotePath
+
+ val operation = ReadFileRemoteOperation(remotePath)
+ val result = operation.execute(user, context)
+
+ if (!result.isSuccess) {
+ val exception = result.exception
+ val message =
+ "Fetching file " + remotePath + " fails with: " + result.getLogMessage(MainApp.getAppContext())
+ Log_OC.e(TAG, exception?.message ?: message)
+
+ return result
+ }
+
+ val remoteFile = result.data[0] as RemoteFile
+
+ // remove file from local db
+ if (removeFileFromDb) {
+ storageManager.removeFile(ocFile, true, true)
+ }
+
+ var ocFile = FileStorageUtils.fillOCFile(remoteFile)
+ FileStorageUtils.searchForLocalFileInDefaultPath(ocFile, user.accountName)
+ ocFile = storageManager.saveFileWithParent(ocFile, context)
+
+ // also sync folder content
+ val toSync: OCFile? = if (ocFile?.isFolder == true) {
+ ocFile
+ } else {
+ ocFile?.parentId?.let { storageManager.getFileById(it) }
+ }
+
+ val currentSyncTime = System.currentTimeMillis()
+ val refreshFolderOperation: RemoteOperation = RefreshFolderOperation(
+ toSync,
+ currentSyncTime,
+ true,
+ true,
+ storageManager,
+ user,
+ context
+ )
+ val refreshOperationResult = refreshFolderOperation.execute(user, context)
+
+ // set the fetched ocFile to resultData to be handled at ui end
+ refreshOperationResult.resultData = ocFile
+
+ return refreshOperationResult
+ }
+ return remoteOperationResult
+ }
+
+ companion object {
+ private val TAG = FetchRemoteFileOperation::class.java.simpleName
+ }
+}
diff --git a/app/src/main/java/com/owncloud/android/operations/albums/CopyFileToAlbumOperation.kt b/app/src/main/java/com/owncloud/android/operations/albums/CopyFileToAlbumOperation.kt
new file mode 100644
index 000000000000..c4d35628f6a5
--- /dev/null
+++ b/app/src/main/java/com/owncloud/android/operations/albums/CopyFileToAlbumOperation.kt
@@ -0,0 +1,76 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2025 TSI-mc
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.owncloud.android.operations.albums
+
+import com.owncloud.android.datamodel.FileDataStorageManager
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.lib.common.OwnCloudClient
+import com.owncloud.android.lib.common.operations.RemoteOperationResult
+import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode
+import com.owncloud.android.lib.resources.albums.CopyFileToAlbumRemoteOperation
+import com.owncloud.android.operations.UploadFileOperation
+import com.owncloud.android.operations.common.SyncOperation
+
+/**
+ * Constructor
+ *
+ * @param srcPath Remote path of the [OCFile] to move.
+ * @param targetParentPath Path to the folder where the file will be copied into.
+ */
+class CopyFileToAlbumOperation(
+ private val srcPath: String,
+ private var targetParentPath: String,
+ storageManager: FileDataStorageManager
+) : SyncOperation(storageManager) {
+ init {
+ if (!targetParentPath.endsWith(OCFile.PATH_SEPARATOR)) {
+ this.targetParentPath += OCFile.PATH_SEPARATOR
+ }
+ }
+
+ /**
+ * Performs the operation.
+ *
+ * @param client Client object to communicate with the remote ownCloud server.
+ */
+ @Deprecated("Deprecated in Java")
+ @Suppress("NestedBlockDepth")
+ override fun run(client: OwnCloudClient): RemoteOperationResult {
+ /** 1. check copy validity */
+ val result: RemoteOperationResult
+
+ if (targetParentPath.startsWith(srcPath)) {
+ result = RemoteOperationResult(ResultCode.INVALID_COPY_INTO_DESCENDANT)
+ } else {
+ val file = storageManager.getFileByPath(srcPath)
+ if (file == null) {
+ result = RemoteOperationResult(ResultCode.FILE_NOT_FOUND)
+ } else {
+ /** 2. remote copy */
+ var targetPath = "$targetParentPath${file.fileName}"
+ if (file.isFolder) {
+ targetPath += OCFile.PATH_SEPARATOR
+ }
+
+ // auto rename, to allow copy
+ if (targetPath == srcPath) {
+ if (file.isFolder) {
+ targetPath = "$targetParentPath${file.fileName}"
+ }
+ targetPath = UploadFileOperation.getNewAvailableRemotePath(client, targetPath, null, false)
+
+ if (file.isFolder) {
+ targetPath += OCFile.PATH_SEPARATOR
+ }
+ }
+
+ result = CopyFileToAlbumRemoteOperation(srcPath, targetPath).execute(client)
+ }
+ }
+ return result
+ }
+}
diff --git a/app/src/main/java/com/owncloud/android/operations/albums/ReadAlbumItemsOperation.kt b/app/src/main/java/com/owncloud/android/operations/albums/ReadAlbumItemsOperation.kt
new file mode 100644
index 000000000000..f4f031d3f3da
--- /dev/null
+++ b/app/src/main/java/com/owncloud/android/operations/albums/ReadAlbumItemsOperation.kt
@@ -0,0 +1,80 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 TSI-mc
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+package com.owncloud.android.operations.albums
+
+import com.nextcloud.common.SessionTimeOut
+import com.nextcloud.common.defaultSessionTimeOut
+import com.owncloud.android.datamodel.FileDataStorageManager
+import com.owncloud.android.lib.common.OwnCloudClient
+import com.owncloud.android.lib.common.operations.RemoteOperation
+import com.owncloud.android.lib.common.operations.RemoteOperationResult
+import com.owncloud.android.lib.resources.albums.ReadAlbumItemsRemoteOperation
+import com.owncloud.android.lib.resources.files.model.RemoteFile
+import com.owncloud.android.utils.FileStorageUtils
+
+class ReadAlbumItemsOperation
+@JvmOverloads
+constructor(
+ private val mRemotePath: String,
+ private val storageManager: FileDataStorageManager?,
+ private val sessionTimeOut: SessionTimeOut = defaultSessionTimeOut
+) : RemoteOperation>() {
+ @Deprecated("Deprecated in Java")
+ @Suppress("TooGenericExceptionCaught")
+ override fun run(client: OwnCloudClient): RemoteOperationResult> {
+ var result: RemoteOperationResult>? = null
+ try {
+ result = ReadAlbumItemsRemoteOperation(mRemotePath, sessionTimeOut).execute(client)
+ if (result.isSuccess) {
+ // get data from remote folder
+ val mFolderAndFiles = saveAlbumData(result.resultData, storageManager)
+
+ // Result of the operation
+ result.apply {
+ // Add data to the result
+ resultData = mFolderAndFiles
+ }
+ }
+ } catch (e: Exception) {
+ result = RemoteOperationResult(e)
+ }
+
+ return result
+ }
+
+ companion object {
+ private val TAG: String = ReadAlbumItemsRemoteOperation::class.java.simpleName
+
+ private fun saveAlbumData(
+ remoteFiles: List,
+ storageManager: FileDataStorageManager?
+ ): List {
+ val files = mutableListOf()
+
+ for (remoteFile in remoteFiles) {
+ // if no fileId received then skip adding
+ if (remoteFile.localId <= 0) {
+ continue
+ }
+ // check if file already exit in db or not
+ // if not then store it in db to show thumbnail and image preview
+ var ocFile = storageManager?.getFileByLocalId(remoteFile.localId)
+ if (ocFile == null) {
+ ocFile = FileStorageUtils.fillOCFile(remoteFile)
+ // no remote id is received from response
+ // so localId will be remoteId else thumb will not generated
+ ocFile.remoteId = remoteFile.localId.toString()
+ ocFile.lastSyncDateForProperties = System.currentTimeMillis()
+ storageManager?.saveFile(ocFile)
+ }
+ files.add(remoteFile)
+ }
+
+ return files
+ }
+ }
+}
diff --git a/app/src/main/java/com/owncloud/android/services/OperationsService.java b/app/src/main/java/com/owncloud/android/services/OperationsService.java
index 6923606819af..6d99e5dd83cf 100644
--- a/app/src/main/java/com/owncloud/android/services/OperationsService.java
+++ b/app/src/main/java/com/owncloud/android/services/OperationsService.java
@@ -2,7 +2,7 @@
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2018-2023 Tobias Kaminsky
- * SPDX-FileCopyrightText: 2021 TSI-mc
+ * SPDX-FileCopyrightText: 2021-2025 TSI-mc
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz
* SPDX-FileCopyrightText: 2017-2018 Andy Scherzinger
* SPDX-FileCopyrightText: 2015 ownCloud Inc.
@@ -42,6 +42,9 @@
import com.owncloud.android.lib.common.operations.RemoteOperation;
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.lib.resources.albums.CreateNewAlbumRemoteOperation;
+import com.owncloud.android.lib.resources.albums.RemoveAlbumRemoteOperation;
+import com.owncloud.android.lib.resources.albums.RenameAlbumRemoteOperation;
import com.owncloud.android.lib.resources.files.RestoreFileVersionRemoteOperation;
import com.owncloud.android.lib.resources.files.model.FileVersion;
import com.owncloud.android.lib.resources.shares.OCShare;
@@ -64,6 +67,7 @@
import com.owncloud.android.operations.UpdateShareInfoOperation;
import com.owncloud.android.operations.UpdateSharePermissionsOperation;
import com.owncloud.android.operations.UpdateShareViaLinkOperation;
+import com.owncloud.android.operations.albums.CopyFileToAlbumOperation;
import java.io.IOException;
import java.util.Optional;
@@ -125,6 +129,11 @@ public class OperationsService extends Service {
public static final String ACTION_CHECK_CURRENT_CREDENTIALS = "CHECK_CURRENT_CREDENTIALS";
public static final String ACTION_RESTORE_VERSION = "RESTORE_VERSION";
public static final String ACTION_UPDATE_FILES_DOWNLOAD_LIMIT = "UPDATE_FILES_DOWNLOAD_LIMIT";
+ public static final String ACTION_CREATE_ALBUM = "CREATE_ALBUM";
+ public static final String EXTRA_ALBUM_NAME = "ALBUM_NAME";
+ public static final String ACTION_ALBUM_COPY_FILE = "ALBUM_COPY_FILE";
+ public static final String ACTION_RENAME_ALBUM = "RENAME_ALBUM";
+ public static final String ACTION_REMOVE_ALBUM = "REMOVE_ALBUM";
private ServiceHandler mOperationsHandler;
private OperationsServiceBinder mOperationsBinder;
@@ -774,6 +783,28 @@ private Pair newOperation(Intent operationIntent) {
}
break;
+ case ACTION_CREATE_ALBUM:
+ String albumName = operationIntent.getStringExtra(EXTRA_ALBUM_NAME);
+ operation = new CreateNewAlbumRemoteOperation(albumName);
+ break;
+
+ case ACTION_ALBUM_COPY_FILE:
+ remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
+ newParentPath = operationIntent.getStringExtra(EXTRA_NEW_PARENT_PATH);
+ operation = new CopyFileToAlbumOperation(remotePath, newParentPath, fileDataStorageManager);
+ break;
+
+ case ACTION_RENAME_ALBUM:
+ remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
+ String newAlbumName = operationIntent.getStringExtra(EXTRA_NEWNAME);
+ operation = new RenameAlbumRemoteOperation(remotePath, newAlbumName);
+ break;
+
+ case ACTION_REMOVE_ALBUM:
+ String albumNameToRemove = operationIntent.getStringExtra(EXTRA_ALBUM_NAME);
+ operation = new RemoveAlbumRemoteOperation(albumNameToRemove);
+ break;
+
default:
// do nothing
break;
diff --git a/app/src/main/java/com/owncloud/android/ui/activity/AlbumsPickerActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/AlbumsPickerActivity.kt
new file mode 100644
index 000000000000..cb96313644e5
--- /dev/null
+++ b/app/src/main/java/com/owncloud/android/ui/activity/AlbumsPickerActivity.kt
@@ -0,0 +1,220 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 TSI-mc
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.owncloud.android.ui.activity
+
+import android.content.Intent
+import android.content.res.Resources
+import android.os.Bundle
+import android.view.MenuItem
+import android.view.View
+import androidx.fragment.app.FragmentActivity
+import com.nextcloud.client.di.Injectable
+import com.owncloud.android.R
+import com.owncloud.android.databinding.FilesFolderPickerBinding
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.lib.common.operations.RemoteOperation
+import com.owncloud.android.lib.common.operations.RemoteOperationResult
+import com.owncloud.android.lib.common.utils.Log_OC
+import com.owncloud.android.lib.resources.albums.CreateNewAlbumRemoteOperation
+import com.owncloud.android.lib.resources.files.SearchRemoteOperation
+import com.owncloud.android.ui.activity.FolderPickerActivity.Companion.TAG_LIST_OF_FOLDERS
+import com.owncloud.android.ui.events.SearchEvent
+import com.owncloud.android.ui.fragment.FileFragment
+import com.owncloud.android.ui.fragment.GalleryFragment
+import com.owncloud.android.ui.fragment.OCFileListFragment
+import com.owncloud.android.ui.fragment.albums.AlbumsFragment
+import com.owncloud.android.utils.DisplayUtils
+import com.owncloud.android.utils.ErrorMessageAdapter
+
+class AlbumsPickerActivity :
+ FileActivity(),
+ FileFragment.ContainerActivity,
+ OnEnforceableRefreshListener,
+ Injectable {
+
+ private var captionText: String? = null
+
+ private var action: String? = null
+
+ private lateinit var folderPickerBinding: FilesFolderPickerBinding
+
+ private fun initBinding() {
+ folderPickerBinding = FilesFolderPickerBinding.inflate(layoutInflater)
+ setContentView(folderPickerBinding.root)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ Log_OC.d(TAG, "onCreate() start")
+
+ super.onCreate(savedInstanceState)
+
+ initBinding()
+ setupToolbar()
+ setupAction()
+ setupActionBar()
+
+ if (savedInstanceState == null) {
+ createFragments()
+ }
+
+ updateActionBarTitleAndHomeButtonByString(captionText)
+ }
+
+ private fun setupActionBar() {
+ findViewById(R.id.sort_list_button_group).visibility =
+ View.GONE
+ findViewById(R.id.switch_grid_view_button).visibility =
+ View.GONE
+ supportActionBar?.let { actionBar ->
+ actionBar.setDisplayHomeAsUpEnabled(true)
+ actionBar.setHomeButtonEnabled(true)
+ captionText?.let {
+ viewThemeUtils.files.themeActionBar(this, actionBar, it)
+ }
+ }
+ }
+
+ private fun setupAction() {
+ action = intent.getStringExtra(EXTRA_ACTION)
+ setupUIForChooseButton()
+ }
+
+ private fun setupUIForChooseButton() {
+ if (action == CHOOSE_ALBUM) {
+ captionText = resources.getText(R.string.album_picker_toolbar_title).toString()
+ } else if (action == CHOOSE_MEDIA_FILES) {
+ captionText = resources.getText(R.string.media_picker_toolbar_title).toString()
+ }
+
+ folderPickerBinding.run {
+ folderPickerBtnCopy.visibility = View.GONE
+ folderPickerBtnMove.visibility = View.GONE
+ folderPickerBtnChoose.visibility = View.GONE
+ folderPickerBtnCancel.visibility = View.GONE
+ chooseButtonSpacer.visibility = View.GONE
+ moveOrCopyButtonSpacer.visibility = View.GONE
+ }
+ }
+
+ private fun createFragments() {
+ if (action == CHOOSE_ALBUM) {
+ val transaction = supportFragmentManager.beginTransaction()
+ transaction.add(
+ R.id.fragment_container,
+ AlbumsFragment.newInstance(isSelectionMode = true),
+ AlbumsFragment.TAG
+ )
+ transaction.commit()
+ } else if (action == CHOOSE_MEDIA_FILES) {
+ createGalleryFragment()
+ }
+ }
+
+ private fun createGalleryFragment() {
+ val bundle = Bundle().apply {
+ putParcelable(
+ OCFileListFragment.SEARCH_EVENT,
+ SearchEvent("image/%", SearchRemoteOperation.SearchType.PHOTO_SEARCH)
+ )
+ putBoolean(EXTRA_FROM_ALBUM, true)
+ }
+
+ val fragment = GalleryFragment().apply {
+ arguments = bundle
+ }
+
+ supportFragmentManager.beginTransaction().run {
+ add(R.id.fragment_container, fragment, TAG_LIST_OF_FOLDERS)
+ commit()
+ }
+ }
+
+ private val listOfFilesFragment: AlbumsFragment?
+ get() {
+ val listOfFiles = supportFragmentManager.findFragmentByTag(AlbumsFragment.TAG)
+
+ return if (listOfFiles != null) {
+ return listOfFiles as AlbumsFragment?
+ } else {
+ Log_OC.e(TAG, "Access to non existing list of albums fragment!!")
+ null
+ }
+ }
+
+ override fun onRemoteOperationFinish(operation: RemoteOperation<*>?, result: RemoteOperationResult<*>) {
+ super.onRemoteOperationFinish(operation, result)
+ if (operation is CreateNewAlbumRemoteOperation) {
+ onCreateAlbumOperationFinish(operation, result)
+ }
+ }
+
+ /**
+ * Updates the view associated to the activity after the finish of an operation trying to create a new folder.
+ *
+ * @param operation Creation operation performed.
+ * @param result Result of the creation.
+ */
+ @Suppress("MaxLineLength")
+ private fun onCreateAlbumOperationFinish(
+ operation: CreateNewAlbumRemoteOperation,
+ result: RemoteOperationResult<*>
+ ) {
+ if (result.isSuccess) {
+ val fileListFragment = listOfFilesFragment
+ fileListFragment?.refreshAlbums()
+ } else {
+ try {
+ DisplayUtils.showSnackMessage(
+ this,
+ ErrorMessageAdapter.getErrorCauseMessage(result, operation, resources)
+ )
+ } catch (e: Resources.NotFoundException) {
+ Log_OC.e(TAG, "Error while trying to show fail message ", e)
+ }
+ }
+ }
+
+ override fun showDetails(file: OCFile?) = Unit
+
+ override fun showDetails(file: OCFile?, activeTab: Int) = Unit
+
+ override fun onBrowsedDownTo(folder: OCFile?) = Unit
+
+ override fun onTransferStateChanged(file: OCFile?, downloading: Boolean, uploading: Boolean) = Unit
+
+ companion object {
+ private val EXTRA_ACTION = AlbumsPickerActivity::class.java.canonicalName?.plus(".EXTRA_ACTION")
+ private val CHOOSE_ALBUM = AlbumsPickerActivity::class.java.canonicalName?.plus(".CHOOSE_ALBUM")
+ private val CHOOSE_MEDIA_FILES = AlbumsPickerActivity::class.java.canonicalName?.plus(".CHOOSE_MEDIA_FILES")
+ val EXTRA_FROM_ALBUM = AlbumsPickerActivity::class.java.canonicalName?.plus(".EXTRA_FROM_ALBUM")
+ val EXTRA_MEDIA_FILES_PATH = AlbumsPickerActivity::class.java.canonicalName?.plus(".EXTRA_MEDIA_FILES_PATH")
+
+ private val TAG = AlbumsPickerActivity::class.java.simpleName
+
+ fun intentForPickingAlbum(context: FragmentActivity): Intent =
+ Intent(context, AlbumsPickerActivity::class.java).apply {
+ putExtra(EXTRA_ACTION, CHOOSE_ALBUM)
+ }
+
+ fun intentForPickingMediaFiles(context: FragmentActivity): Intent =
+ Intent(context, AlbumsPickerActivity::class.java).apply {
+ putExtra(EXTRA_ACTION, CHOOSE_MEDIA_FILES)
+ }
+ }
+
+ override fun onRefresh(enforced: Boolean) = Unit
+
+ override fun onRefresh() = Unit
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ when (item.itemId) {
+ android.R.id.home -> super.onBackPressed()
+ }
+ return super.onOptionsItemSelected(item)
+ }
+}
diff --git a/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java
index 4f089971528f..7dfcb7552791 100644
--- a/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java
+++ b/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java
@@ -1,7 +1,7 @@
/*
* Nextcloud - Android Client
*
- * SPDX-FileCopyrightText: 2021-2024 TSI-mc
+ * SPDX-FileCopyrightText: 2021-2026 TSI-mc
* SPDX-FileCopyrightText: 2020 Infomaniak Network SA
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz
* SPDX-FileCopyrightText: 2017 Tobias Kaminsky
@@ -96,6 +96,8 @@
import com.owncloud.android.ui.fragment.GroupfolderListFragment;
import com.owncloud.android.ui.fragment.OCFileListFragment;
import com.owncloud.android.ui.fragment.SharedListFragment;
+import com.owncloud.android.ui.fragment.albums.AlbumItemsFragment;
+import com.owncloud.android.ui.fragment.albums.AlbumsFragment;
import com.owncloud.android.ui.preview.PreviewTextStringFragment;
import com.owncloud.android.ui.trashbin.TrashbinActivity;
import com.owncloud.android.utils.BitmapUtils;
@@ -128,6 +130,7 @@
import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentTransaction;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hct.Hct;
import kotlin.Unit;
@@ -543,7 +546,8 @@ private void onNavigationItemClicked(final MenuItem menuItem) {
!(fda.getLeftFragment() instanceof GalleryFragment) &&
!(fda.getLeftFragment() instanceof SharedListFragment) &&
!(fda.getLeftFragment() instanceof GroupfolderListFragment) &&
- !(fda.getLeftFragment() instanceof PreviewTextStringFragment)) {
+ !(fda.getLeftFragment() instanceof PreviewTextStringFragment) &&
+ !isAlbumsFragment() && !isAlbumItemsFragment()) {
showFiles(false, itemId == R.id.nav_personal_files);
fda.browseToRoot();
EventBus.getDefault().post(new ChangeMenuEvent());
@@ -563,6 +567,17 @@ private void onNavigationItemClicked(final MenuItem menuItem) {
openFavoritesTab();
} else if (itemId == R.id.nav_gallery) {
openMediaTab(menuItem.getItemId());
+ } else if (itemId == R.id.nav_album) {
+ if (this instanceof FileDisplayActivity) {
+ replaceAlbumFragment();
+ } else {
+ // when user is not on FileDisplayActivity
+ // if user is on TrashbinActivity then we have to start activity again
+ Intent intent = new Intent(getApplicationContext(), FileDisplayActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ intent.setAction(FileDisplayActivity.ALBUMS);
+ startActivity(intent);
+ }
} else if (itemId == R.id.nav_on_device) {
showOnDeviceFiles();
} else if (itemId == R.id.nav_uploads) {
@@ -640,6 +655,8 @@ private void handleBottomNavigationViewClicks() {
startAssistantScreen();
} else if (menuItemId == R.id.nav_gallery) {
openMediaTab(menuItem.getItemId());
+ } else if (menuItemId == R.id.nav_album) {
+ replaceAlbumFragment();
}
// Remove extra icon from the action bar
@@ -664,6 +681,32 @@ private void resetFileDepthAndConfigureMenuItem() {
}
}
+ public void replaceAlbumFragment() {
+ if (isAlbumsFragment()) {
+ return;
+ }
+ FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
+ transaction.addToBackStack(null);
+ transaction.replace(R.id.left_fragment_container, AlbumsFragment.Companion.newInstance(false), AlbumsFragment.Companion.getTAG());
+ transaction.commit();
+ }
+
+ public Optional getFragment(String tag, Class clazz) {
+ return Optional.ofNullable(getSupportFragmentManager().findFragmentByTag(tag))
+ .filter(clazz::isInstance)
+ .map(clazz::cast);
+ }
+
+ public boolean isAlbumsFragment() {
+ final var fragment = getFragment(AlbumsFragment.Companion.getTAG(), AlbumsFragment.class);
+ return fragment.map(AlbumsFragment::isVisible).orElse(false);
+ }
+
+ public boolean isAlbumItemsFragment() {
+ final var fragment = getFragment(AlbumItemsFragment.Companion.getTAG(), AlbumItemsFragment.class);
+ return fragment.map(AlbumItemsFragment::isVisible).orElse(false);
+ }
+
private void startAssistantScreen() {
final var destination = ComposeDestination.Companion.getAssistantScreen(this);
Intent composeActivity = new Intent(getApplicationContext(), ComposeActivity.class);
diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java
index 47e079c1f67e..ad616579e651 100644
--- a/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java
+++ b/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java
@@ -1,7 +1,7 @@
/*
* Nextcloud - Android Client
*
- * SPDX-FileCopyrightText: 2021 TSI-mc
+ * SPDX-FileCopyrightText: 2021-2026 TSI-mc
* SPDX-FileCopyrightText: 2022 Álvaro Brey
* SPDX-FileCopyrightText: 2017-2023 Tobias Kaminsky
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz
@@ -88,10 +88,11 @@
import com.owncloud.android.ui.dialog.SslUntrustedCertDialog;
import com.owncloud.android.ui.events.DialogEvent;
import com.owncloud.android.ui.events.DialogEventType;
-import com.owncloud.android.ui.events.FavoriteEvent;
import com.owncloud.android.ui.fragment.FileDetailFragment;
import com.owncloud.android.ui.fragment.FileDetailSharingFragment;
import com.owncloud.android.ui.fragment.OCFileListFragment;
+import com.owncloud.android.ui.fragment.albums.AlbumItemsFragment;
+import com.owncloud.android.ui.fragment.albums.AlbumsFragment;
import com.owncloud.android.ui.fragment.filesRepository.FilesRepository;
import com.owncloud.android.ui.fragment.filesRepository.RemoteFilesRepository;
import com.owncloud.android.ui.helpers.FileOperationsHelper;
@@ -839,7 +840,21 @@ private void onUpdateShareInformation(RemoteOperationResult result, @StringRes i
}
public void refreshList() {
- final Fragment fragment = getSupportFragmentManager().findFragmentByTag(FileDisplayActivity.TAG_LIST_OF_FILES);
+ if (isAlbumsFragment()) {
+ getFragment(AlbumsFragment.Companion.getTAG(), AlbumsFragment.class)
+ .ifPresent(AlbumsFragment::refreshAlbums);
+ return;
+ }
+
+ if (isAlbumItemsFragment()) {
+ getFragment(AlbumItemsFragment.Companion.getTAG(), AlbumItemsFragment.class)
+ .ifPresent(AlbumItemsFragment::refreshData);
+ return;
+ }
+
+ final var fragment =
+ getSupportFragmentManager().findFragmentByTag(FileDisplayActivity.TAG_LIST_OF_FILES);
+
if (fragment instanceof OCFileListFragment listFragment) {
listFragment.onRefresh();
} else if (fragment instanceof FileDetailFragment detailFragment) {
diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt
index 79925570bbac..c380648f3684 100644
--- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt
+++ b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt
@@ -2,7 +2,7 @@
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2025 Alper Ozturk
- * SPDX-FileCopyrightText: 2023-2024 TSI-mc
+ * SPDX-FileCopyrightText: 2023-2026 TSI-mc
* SPDX-FileCopyrightText: 2023 Archontis E. Kostis
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz
* SPDX-FileCopyrightText: 2018-2022 Tobias Kaminsky
@@ -72,6 +72,7 @@ import com.nextcloud.client.media.PlayerServiceConnection
import com.nextcloud.client.network.ClientFactory.CreationException
import com.nextcloud.client.preferences.AppPreferences
import com.nextcloud.client.utils.IntentUtil
+import com.nextcloud.model.OCUploadLocalPathData
import com.nextcloud.model.WorkerState
import com.nextcloud.model.WorkerState.FileDownloadCompleted
import com.nextcloud.model.WorkerState.FileDownloadStarted
@@ -91,11 +92,13 @@ import com.owncloud.android.datamodel.FileDataStorageManager
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.datamodel.SyncedFolderProvider
import com.owncloud.android.datamodel.VirtualFolderType
-import com.owncloud.android.files.services.NameCollisionPolicy
import com.owncloud.android.lib.common.OwnCloudClient
import com.owncloud.android.lib.common.operations.RemoteOperation
import com.owncloud.android.lib.common.operations.RemoteOperationResult
import com.owncloud.android.lib.common.utils.Log_OC
+import com.owncloud.android.lib.resources.albums.CreateNewAlbumRemoteOperation
+import com.owncloud.android.lib.resources.albums.RemoveAlbumRemoteOperation
+import com.owncloud.android.lib.resources.albums.RenameAlbumRemoteOperation
import com.owncloud.android.lib.resources.files.RestoreFileVersionRemoteOperation
import com.owncloud.android.lib.resources.files.SearchRemoteOperation
import com.owncloud.android.lib.resources.notifications.GetNotificationsRemoteOperation
@@ -107,7 +110,7 @@ import com.owncloud.android.operations.RefreshFolderOperation
import com.owncloud.android.operations.RemoveFileOperation
import com.owncloud.android.operations.RenameFileOperation
import com.owncloud.android.operations.SynchronizeFileOperation
-import com.owncloud.android.operations.UploadFileOperation
+import com.owncloud.android.operations.albums.CopyFileToAlbumOperation
import com.owncloud.android.syncadapter.FileSyncAdapter
import com.owncloud.android.ui.CompletionCallback
import com.owncloud.android.ui.asynctasks.CheckAvailableSpaceTask
@@ -133,6 +136,8 @@ import com.owncloud.android.ui.fragment.SearchType
import com.owncloud.android.ui.fragment.SharedListFragment
import com.owncloud.android.ui.fragment.TaskRetainerFragment
import com.owncloud.android.ui.fragment.UnifiedSearchFragment
+import com.owncloud.android.ui.fragment.albums.AlbumItemsFragment
+import com.owncloud.android.ui.fragment.albums.AlbumsFragment
import com.owncloud.android.ui.helpers.FileOperationsHelper
import com.owncloud.android.ui.helpers.UriUploader
import com.owncloud.android.ui.interfaces.TransactionInterface
@@ -571,7 +576,9 @@ class FileDisplayActivity :
// Using `is OCFileListFragment` would also match subclasses,
// its needed because reinitializing OCFileListFragment itself causes an empty screen
leftFragment?.let {
- if (it::class != OCFileListFragment::class) {
+ // check for albums fragment to load All Files
+ // when user is on AlbumsFragment and click on All Files
+ if (it::class != OCFileListFragment::class || isAlbumsFragment()) {
leftFragment = OCFileListFragment()
supportFragmentManager.executePendingTransactions()
}
@@ -586,6 +593,12 @@ class FileDisplayActivity :
leftFragment = GroupfolderListFragment()
supportFragmentManager.executePendingTransactions()
}
+ ALBUMS == action -> {
+ Log_OC.d(this, "Switch to list albums fragment")
+ menuItemId = R.id.nav_album
+ replaceAlbumFragment()
+ supportFragmentManager.executePendingTransactions()
+ }
ON_DEVICE == action -> {
refreshOrInitOCFileListFragment()
@@ -963,7 +976,8 @@ class FileDisplayActivity :
private fun shouldOpenDrawer(): Boolean = !isDrawerOpen &&
!isSearchOpen() &&
isRoot(getCurrentDir()) &&
- this.leftFragment is OCFileListFragment
+ this.leftFragment is OCFileListFragment &&
+ !isAlbumItemsFragment()
/**
* Called, when the user selected something for uploading
@@ -1084,19 +1098,15 @@ class FileDisplayActivity :
return@isNetworkAndServerAvailable
}
- FileUploadHelper.Companion.instance().uploadNewFiles(
- user.orElseThrow(
- Supplier { RuntimeException() }
- ),
+ val data = OCUploadLocalPathData.forFile(
+ user.orElseThrow(Supplier { RuntimeException() }),
filePaths,
decryptedRemotePaths,
behaviour,
- true,
- UploadFileOperation.CREATED_BY_USER,
- false,
- false,
- NameCollisionPolicy.ASK_USER
+ createRemoteFolder = true
)
+
+ FileUploadHelper.instance().uploadNewFiles(data)
} else {
fileDataStorageManager.addCreateFileOfflineOperation(filePaths, decryptedRemotePaths)
}
@@ -1199,6 +1209,13 @@ class FileDisplayActivity :
after()
}
+ // pop back if current fragment is AlbumItemsFragment
+ isAlbumItemsFragment() -> {
+ before()
+ popBack()
+ after()
+ }
+
leftFragment is OCFileListFragment -> {
before()
handleOCFileListFragmentBackPress()
@@ -1734,6 +1751,13 @@ class FileDisplayActivity :
}
}
}
+
+ // notify when upload is finished and user is on albums screen
+ if (isAlbumsFragment()) {
+ (supportFragmentManager.findFragmentByTag(AlbumsFragment.TAG) as AlbumsFragment).refreshAlbums()
+ } else if (isAlbumItemsFragment()) {
+ (supportFragmentManager.findFragmentByTag(AlbumItemsFragment.TAG) as AlbumItemsFragment).refreshData()
+ }
}
// TODO refactor this receiver, and maybe DownloadFinishReceiver; this method is duplicated :S
@@ -2066,6 +2090,22 @@ class FileDisplayActivity :
is RestoreFileVersionRemoteOperation -> {
onRestoreFileVersionOperationFinish(result)
}
+
+ is CreateNewAlbumRemoteOperation -> {
+ onCreateAlbumOperationFinish(operation, result)
+ }
+
+ is CopyFileToAlbumOperation -> {
+ onCopyAlbumFileOperationFinish(operation, result)
+ }
+
+ is RenameAlbumRemoteOperation -> {
+ onRenameAlbumOperationFinish(operation, result)
+ }
+
+ is RemoveAlbumRemoteOperation -> {
+ onRemoveAlbumOperationFinish(operation, result)
+ }
}
}
@@ -2314,6 +2354,92 @@ class FileDisplayActivity :
}
}
+ private fun onRemoveAlbumOperationFinish(operation: RemoveAlbumRemoteOperation, result: RemoteOperationResult<*>) {
+ if (result.isSuccess) {
+ val fragment = supportFragmentManager.findFragmentByTag(AlbumItemsFragment.TAG)
+ if (fragment is AlbumItemsFragment) {
+ fragment.onAlbumDeleted()
+ }
+ } else {
+ DisplayUtils.showSnackMessage(
+ this,
+ ErrorMessageAdapter.getErrorCauseMessage(result, operation, getResources())
+ )
+
+ if (result.isSslRecoverableException) {
+ mLastSslUntrustedServerResult = result
+ showUntrustedCertDialog(mLastSslUntrustedServerResult)
+ }
+ }
+ }
+
+ private fun onCopyAlbumFileOperationFinish(operation: CopyFileToAlbumOperation, result: RemoteOperationResult<*>) {
+ if (result.isSuccess) {
+ // when item added from inside of Album
+ val fragment = supportFragmentManager.findFragmentByTag(AlbumItemsFragment.TAG)
+ if (fragment is AlbumItemsFragment) {
+ fragment.refreshData()
+ } else {
+ // files added directly from Media tab
+ DisplayUtils.showSnackMessage(this, getResources().getString(R.string.album_file_added_message))
+ }
+ Log_OC.e(TAG, "Files copied successfully")
+ } else {
+ try {
+ DisplayUtils.showSnackMessage(
+ this,
+ ErrorMessageAdapter.getErrorCauseMessage(result, operation, getResources())
+ )
+ } catch (e: Resources.NotFoundException) {
+ Log_OC.e(TAG, "Error while trying to show fail message ", e)
+ }
+ }
+ }
+
+ private fun onRenameAlbumOperationFinish(operation: RenameAlbumRemoteOperation, result: RemoteOperationResult<*>) {
+ if (result.isSuccess) {
+ val fragment = supportFragmentManager.findFragmentByTag(AlbumItemsFragment.TAG)
+ if (fragment is AlbumItemsFragment) {
+ fragment.onAlbumRenamed(operation.newAlbumName)
+ }
+ } else {
+ DisplayUtils.showSnackMessage(
+ this,
+ ErrorMessageAdapter.getErrorCauseMessage(result, operation, getResources())
+ )
+
+ if (result.isSslRecoverableException) {
+ mLastSslUntrustedServerResult = result
+ showUntrustedCertDialog(mLastSslUntrustedServerResult)
+ }
+ }
+ }
+
+ private fun onCreateAlbumOperationFinish(
+ operation: CreateNewAlbumRemoteOperation,
+ result: RemoteOperationResult<*>
+ ) {
+ if (result.isSuccess) {
+ val fragment = supportFragmentManager.findFragmentByTag(AlbumsFragment.TAG)
+ if (fragment is AlbumsFragment) {
+ fragment.navigateToAlbumItemsFragment(operation.newAlbumName, true)
+ }
+ } else {
+ try {
+ if (RemoteOperationResult.ResultCode.FOLDER_ALREADY_EXISTS == result.code) {
+ DisplayUtils.showSnackMessage(this, R.string.album_already_exists)
+ } else {
+ DisplayUtils.showSnackMessage(
+ this,
+ ErrorMessageAdapter.getErrorCauseMessage(result, operation, getResources())
+ )
+ }
+ } catch (e: Resources.NotFoundException) {
+ Log_OC.e(TAG, "Error while trying to show fail message ", e)
+ }
+ }
+ }
+
/**
* {@inheritDoc}
*/
@@ -2738,7 +2864,10 @@ class FileDisplayActivity :
val ocFileListFragment = this.listOfFilesFragment
if (ocFileListFragment != null &&
(ocFileListFragment !is GalleryFragment) &&
- (ocFileListFragment !is SharedListFragment)
+ (ocFileListFragment !is SharedListFragment) &&
+ // album fragment check will help in showing offline files screen
+ // when navigating from Albums to Offline Files
+ !isAlbumsFragment && !isAlbumItemsFragment
) {
ocFileListFragment.refreshDirectory()
} else {
@@ -2812,6 +2941,7 @@ class FileDisplayActivity :
initFile()
}
+ @Suppress("LongMethod")
private fun initFile() {
val userOpt = user
if (userOpt.isEmpty) {
@@ -3103,6 +3233,7 @@ class FileDisplayActivity :
const val RESTART: String = "RESTART"
const val ALL_FILES: String = "ALL_FILES"
const val LIST_GROUPFOLDERS: String = "LIST_GROUPFOLDERS"
+ const val ALBUMS: String = "ALBUMS"
const val SINGLE_USER_SIZE: Int = 1
const val OPEN_FILE: String = "NC_OPEN_FILE"
const val ON_DEVICE = "ON_DEVICE"
diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java
index 2e8a6a27cdbf..4337f773fff6 100755
--- a/app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java
+++ b/app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java
@@ -51,6 +51,7 @@
import com.nextcloud.client.jobs.upload.FileUploadHelper;
import com.nextcloud.client.jobs.upload.FileUploadWorker;
import com.nextcloud.client.preferences.AppPreferences;
+import com.nextcloud.model.OCUploadLocalPathData;
import com.nextcloud.utils.extensions.BundleExtensionsKt;
import com.nextcloud.utils.extensions.FileExtensionsKt;
import com.nextcloud.utils.extensions.IntentExtensionsKt;
@@ -930,16 +931,16 @@ private boolean somethingToUpload() {
}
public void uploadFile(String tmpName, String filename) {
- FileUploadHelper.Companion.instance().uploadNewFiles(
- getUser().orElseThrow(RuntimeException::new),
- new String[]{ tmpName },
- new String[]{ mFile.getRemotePath() + filename},
- FileUploadWorker.LOCAL_BEHAVIOUR_COPY,
- true,
- UploadFileOperation.CREATED_BY_USER,
- false,
- false,
- NameCollisionPolicy.ASK_USER);
+ final var data = new OCUploadLocalPathData(getUser().orElseThrow(RuntimeException::new),
+ new String[]{ tmpName },
+ new String[]{ mFile.getRemotePath() + filename},
+ FileUploadWorker.LOCAL_BEHAVIOUR_COPY,
+ true,
+ UploadFileOperation.CREATED_BY_USER,
+ false,
+ false,
+ NameCollisionPolicy.ASK_USER);
+ FileUploadHelper.Companion.instance().uploadNewFiles(data);
finish();
}
diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/GalleryAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/GalleryAdapter.kt
index edbbe0d4a19d..77803aef003a 100644
--- a/app/src/main/java/com/owncloud/android/ui/adapter/GalleryAdapter.kt
+++ b/app/src/main/java/com/owncloud/android/ui/adapter/GalleryAdapter.kt
@@ -6,7 +6,7 @@
* @author TSI-mc
* Copyright (C) 2022 Tobias Kaminsky
* Copyright (C) 2022 Nextcloud GmbH
- * Copyright (C) 2023 TSI-mc
+ * Copyright (C) 2023-2025 TSI-mc
*
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
@@ -253,6 +253,12 @@ class GalleryAdapter(
notifyDataSetChanged()
}
+ @SuppressLint("NotifyDataSetChanged")
+ fun showAlbumItems(albumItems: List) {
+ files = albumItems.toGalleryItems()
+ notifyDataSetChanged()
+ }
+
private fun transformToRows(list: List): List {
if (list.isEmpty()) return emptyList()
@@ -314,6 +320,10 @@ class GalleryAdapter(
}
}
+ fun setCheckedItem(files: Set?) {
+ ocFileListDelegate.setCheckedItem(files)
+ }
+
override fun setMultiSelect(boolean: Boolean) {
ocFileListDelegate.isMultiSelect = boolean
}
diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/LinkShareViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/LinkShareViewHolder.kt
index 3ebe6b08d380..a58fbba63a1a 100644
--- a/app/src/main/java/com/owncloud/android/ui/adapter/LinkShareViewHolder.kt
+++ b/app/src/main/java/com/owncloud/android/ui/adapter/LinkShareViewHolder.kt
@@ -98,6 +98,7 @@ internal class LinkShareViewHolder(itemView: View) : RecyclerView.ViewHolder(ite
}
}
+ @Suppress("ReturnCount")
private fun setSubline(binding: FileDetailsShareLinkShareItemBinding?, context: Context?, publicShare: OCShare) {
if (binding == null || context == null) {
return
diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt
index 3d3437785e5d..7a2e5ac96dc0 100644
--- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt
+++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt
@@ -1,6 +1,7 @@
/*
* Nextcloud - Android Client
*
+ * SPDX-FileCopyrightText: 2025 TSI-mc
* SPDX-FileCopyrightText: 2022 Tobias Kaminsky
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
@@ -34,6 +35,8 @@ import com.owncloud.android.ui.activity.ComponentsGetter
import com.owncloud.android.ui.activity.FolderPickerActivity
import com.owncloud.android.ui.fragment.GalleryFragment
import com.owncloud.android.ui.fragment.SearchType
+import com.owncloud.android.ui.fragment.albums.AlbumItemsFragment
+import com.owncloud.android.ui.activity.AlbumsPickerActivity
import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface
import com.owncloud.android.utils.DisplayUtils
import com.owncloud.android.utils.EncryptionUtils
@@ -159,8 +162,16 @@ class OCFileListDelegate(
GalleryImageGenerationJob.storeJob(job, imageView)
imageView.setOnClickListener {
- ocFileListFragmentInterface.onItemClicked(file)
- GalleryFragment.setLastMediaItemPosition(galleryRowHolder.absoluteAdapterPosition)
+ // while picking media directly perform long click
+ if (context is AlbumsPickerActivity) {
+ ocFileListFragmentInterface.onLongItemClicked(
+ file
+ )
+ } else {
+ ocFileListFragmentInterface.onItemClicked(file)
+ GalleryFragment.setLastMediaItemPosition(galleryRowHolder.absoluteAdapterPosition)
+ AlbumItemsFragment.lastMediaItemPosition = galleryRowHolder.absoluteAdapterPosition
+ }
}
if (!hideItemOptions) {
@@ -187,7 +198,8 @@ class OCFileListDelegate(
shimmerThumbnail,
preferences,
viewThemeUtils,
- syncFolderProvider
+ syncFolderProvider,
+ false
)
}
diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchCurrentDirItemViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchCurrentDirItemViewHolder.kt
index a355cd5d4d0b..24939735fd77 100644
--- a/app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchCurrentDirItemViewHolder.kt
+++ b/app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchCurrentDirItemViewHolder.kt
@@ -61,7 +61,8 @@ class UnifiedSearchCurrentDirItemViewHolder(
binding.thumbnailShimmer,
appPreferences,
viewThemeUtils,
- syncedFolderProvider
+ syncedFolderProvider,
+ false
)
binding.more.setOnClickListener {
diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/albums/AlbumFragmentInterface.kt b/app/src/main/java/com/owncloud/android/ui/adapter/albums/AlbumFragmentInterface.kt
new file mode 100644
index 000000000000..652295388c68
--- /dev/null
+++ b/app/src/main/java/com/owncloud/android/ui/adapter/albums/AlbumFragmentInterface.kt
@@ -0,0 +1,14 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2025 TSI-mc
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.owncloud.android.ui.adapter.albums
+
+import com.owncloud.android.lib.resources.albums.PhotoAlbumEntry
+
+interface AlbumFragmentInterface {
+ fun onItemClick(album: PhotoAlbumEntry)
+}
diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/albums/AlbumGridItemViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/albums/AlbumGridItemViewHolder.kt
new file mode 100644
index 000000000000..c2dea316e85e
--- /dev/null
+++ b/app/src/main/java/com/owncloud/android/ui/adapter/albums/AlbumGridItemViewHolder.kt
@@ -0,0 +1,27 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2025 TSI-mc
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.owncloud.android.ui.adapter.albums
+
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import com.elyeproj.loaderviewlibrary.LoaderImageView
+import com.owncloud.android.databinding.AlbumsGridItemBinding
+
+internal class AlbumGridItemViewHolder(private var binding: AlbumsGridItemBinding) :
+ RecyclerView.ViewHolder(binding.root),
+ AlbumItemViewHolder {
+ override val thumbnail: ImageView
+ get() = binding.thumbnail
+ override val shimmerThumbnail: LoaderImageView
+ get() = binding.thumbnailShimmer
+ override val albumName: TextView
+ get() = binding.Filename
+ override val albumInfo: TextView
+ get() = binding.fileInfo
+}
diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/albums/AlbumItemViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/albums/AlbumItemViewHolder.kt
new file mode 100644
index 000000000000..a531404e8e99
--- /dev/null
+++ b/app/src/main/java/com/owncloud/android/ui/adapter/albums/AlbumItemViewHolder.kt
@@ -0,0 +1,19 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2025 TSI-mc
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.owncloud.android.ui.adapter.albums
+
+import android.widget.ImageView
+import android.widget.TextView
+import com.elyeproj.loaderviewlibrary.LoaderImageView
+
+interface AlbumItemViewHolder {
+ val thumbnail: ImageView
+ val shimmerThumbnail: LoaderImageView
+ val albumName: TextView
+ val albumInfo: TextView
+}
diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/albums/AlbumListItemViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/albums/AlbumListItemViewHolder.kt
new file mode 100644
index 000000000000..a1bfa865942b
--- /dev/null
+++ b/app/src/main/java/com/owncloud/android/ui/adapter/albums/AlbumListItemViewHolder.kt
@@ -0,0 +1,27 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2025 TSI-mc
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.owncloud.android.ui.adapter.albums
+
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import com.elyeproj.loaderviewlibrary.LoaderImageView
+import com.owncloud.android.databinding.AlbumsListItemBinding
+
+internal class AlbumListItemViewHolder(private var binding: AlbumsListItemBinding) :
+ RecyclerView.ViewHolder(binding.root),
+ AlbumItemViewHolder {
+ override val thumbnail: ImageView
+ get() = binding.thumbnail
+ override val shimmerThumbnail: LoaderImageView
+ get() = binding.thumbnailShimmer
+ override val albumName: TextView
+ get() = binding.Filename
+ override val albumInfo: TextView
+ get() = binding.fileInfo
+}
diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/albums/AlbumsAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/albums/AlbumsAdapter.kt
new file mode 100644
index 000000000000..fa4be74ae6be
--- /dev/null
+++ b/app/src/main/java/com/owncloud/android/ui/adapter/albums/AlbumsAdapter.kt
@@ -0,0 +1,118 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 TSI-mc
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.owncloud.android.ui.adapter.albums
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import com.nextcloud.client.account.User
+import com.nextcloud.client.preferences.AppPreferences
+import com.owncloud.android.R
+import com.owncloud.android.databinding.AlbumsGridItemBinding
+import com.owncloud.android.databinding.AlbumsListItemBinding
+import com.owncloud.android.datamodel.FileDataStorageManager
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.datamodel.SyncedFolderProvider
+import com.owncloud.android.datamodel.ThumbnailsCacheManager
+import com.owncloud.android.lib.common.utils.Log_OC
+import com.owncloud.android.lib.resources.albums.PhotoAlbumEntry
+import com.owncloud.android.utils.DisplayUtils
+import com.owncloud.android.utils.theme.ViewThemeUtils
+
+@Suppress("LongParameterList")
+class AlbumsAdapter(
+ val context: Context,
+ private val storageManager: FileDataStorageManager?,
+ private val user: User,
+ private val albumFragmentInterface: AlbumFragmentInterface,
+ private val syncedFolderProvider: SyncedFolderProvider,
+ private val preferences: AppPreferences,
+ private val viewThemeUtils: ViewThemeUtils,
+ private val gridView: Boolean = true
+) : RecyclerView.Adapter() {
+ private var albumList: MutableList = mutableListOf()
+ private val asyncTasks: MutableList = ArrayList()
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = if (gridView) {
+ AlbumGridItemViewHolder(AlbumsGridItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
+ } else {
+ AlbumListItemViewHolder(AlbumsListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
+ }
+
+ override fun getItemCount(): Int = albumList.size
+
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ val gridViewHolder = holder as AlbumItemViewHolder
+ val file: PhotoAlbumEntry = albumList[position]
+
+ gridViewHolder.albumName.text = file.albumName
+ gridViewHolder.thumbnail.tag = file.lastPhoto
+ gridViewHolder.albumInfo.text = String.format(
+ context.resources.getString(R.string.album_items_text),
+ file.nbItems,
+ DisplayUtils.getDateByPattern(file.createdDate, "MMM yyyy")
+ )
+
+ if (file.lastPhoto > 0) {
+ var ocLocal = storageManager?.getFileByLocalId(file.lastPhoto)
+ if (ocLocal == null) {
+ // if local file is not present make dummy file with fake remotePath
+ // without remotePath it won't work
+ // lastPhoto is file id which we can set it to localId and remoteId for thumbnail generation
+ val nFile = OCFile("/" + file.albumName)
+ nFile.localId = file.lastPhoto
+ nFile.remoteId = file.lastPhoto.toString()
+ ocLocal = nFile
+ }
+ DisplayUtils.setThumbnail(
+ ocLocal,
+ gridViewHolder.thumbnail,
+ user,
+ storageManager,
+ asyncTasks,
+ gridView,
+ context,
+ gridViewHolder.shimmerThumbnail,
+ preferences,
+ viewThemeUtils,
+ syncedFolderProvider,
+ true
+ )
+ } else {
+ gridViewHolder.thumbnail.setImageResource(R.drawable.file_image)
+ gridViewHolder.thumbnail.visibility = View.VISIBLE
+ gridViewHolder.shimmerThumbnail.visibility = View.GONE
+ }
+
+ holder.itemView.setOnClickListener { albumFragmentInterface.onItemClick(file) }
+ }
+
+ fun cancelAllPendingTasks() {
+ for (task in asyncTasks) {
+ task.cancel(true)
+ if (task.getMethod != null) {
+ Log_OC.d("AlbumsAdapter", "cancel: abort get method directly")
+ task.getMethod.abort()
+ }
+ }
+ asyncTasks.clear()
+ }
+
+ @SuppressLint("NotifyDataSetChanged")
+ fun setAlbumItems(albumItems: List?) {
+ albumList.clear()
+ albumItems?.let {
+ // alphabetically sorting
+ albumList.addAll(it.sortedBy { album -> album.albumName.lowercase() })
+ }
+ notifyDataSetChanged()
+ }
+}
diff --git a/app/src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.java b/app/src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.java
index 01d683a71b1e..f20bf524eecf 100644
--- a/app/src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.java
+++ b/app/src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.java
@@ -8,6 +8,7 @@
* SPDX-FileCopyrightText: 2016 Juan Carlos González Cabrero
* SPDX-FileCopyrightText: 2015 María Asensio Valverde
* SPDX-FileCopyrightText: 2014 David A. Velasco
+ * SPDX-FileCopyrightText: 2026 TSI-mc
* SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only)
*/
package com.owncloud.android.ui.asynctasks;
@@ -18,15 +19,15 @@
import android.net.Uri;
import android.os.AsyncTask;
import android.provider.DocumentsContract;
+import android.text.TextUtils;
import android.widget.Toast;
import com.nextcloud.client.account.User;
import com.nextcloud.client.jobs.upload.FileUploadHelper;
+import com.nextcloud.model.OCUploadLocalPathData;
import com.owncloud.android.R;
-import com.owncloud.android.files.services.NameCollisionPolicy;
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.utils.FileStorageUtils;
import java.io.File;
@@ -55,6 +56,9 @@ public class CopyAndUploadContentUrisTask extends AsyncTask