Skip to content

Albums functionality #14947

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<!--
~ Nextcloud - Android Client
~
~ SPDX-FileCopyrightText: 2024 TSI-mc <surinder.kumar@t-systems.com>
~ SPDX-FileCopyrightText: 2024-2025 TSI-mc <surinder.kumar@t-systems.com>
~ SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
~ SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
-->
Expand Down Expand Up @@ -592,6 +592,9 @@
android:launchMode="singleTop"
android:theme="@style/Theme.ownCloud.Dialog.NoTitle"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".ui.activity.AlbumsPickerActivity"
android:exported="false" />
<activity
android:name=".ui.activity.ShareActivity"
android:exported="false"
Expand Down
22 changes: 21 additions & 1 deletion app/src/main/java/com/nextcloud/client/di/ComponentsModule.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 TSI-mc <surinder.kumar@t-systems.com>
* SPDX-FileCopyrightText: 2024-2025 TSI-mc <surinder.kumar@t-systems.com>
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
Expand Down Expand Up @@ -31,6 +31,7 @@
import com.nextcloud.ui.ChooseStorageLocationDialogFragment;
import com.nextcloud.ui.ImageDetailFragment;
import com.nextcloud.ui.SetStatusDialogFragment;
import com.nextcloud.ui.albumItemActions.AlbumItemActionsBottomSheet;
import com.nextcloud.ui.composeActivity.ComposeActivity;
import com.nextcloud.ui.fileactions.FileActionsBottomSheet;
import com.nextcloud.ui.trashbinFileActions.TrashbinFileActionsBottomSheet;
Expand Down Expand Up @@ -81,6 +82,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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -505,4 +510,19 @@ abstract class ComponentsModule {

@ContributesAndroidInjector
abstract TermsOfServiceDialog termsOfServiceDialog();

@ContributesAndroidInjector
abstract AlbumsPickerActivity albumsPickerActivity();

@ContributesAndroidInjector
abstract CreateAlbumDialogFragment createAlbumDialogFragment();

@ContributesAndroidInjector
abstract AlbumsFragment albumsFragment();

@ContributesAndroidInjector
abstract AlbumItemsFragment albumItemsFragment();

@ContributesAndroidInjector
abstract AlbumItemActionsBottomSheet albumItemActionsBottomSheet();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2025 TSI-mc <surinder.kumar@t-systems.com>
* 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
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2025 TSI-mc <surinder.kumar@t-systems.com>
* 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 {
return AlbumItemActionsBottomSheet()
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2025 TSI-mc <surinder.kumar@t-systems.com>
* SPDX-FileCopyrightText: 2017 Tobias Kaminsky <tobias@kaminsky.me>
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
Expand All @@ -12,5 +13,5 @@
* Type for virtual folders
*/
public enum VirtualFolderType {
FAVORITE, GALLERY, NONE
FAVORITE, GALLERY, ALBUM, NONE
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2025 TSI-mc <surinder.kumar@t-systems.com>
* 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<Any> {
/** 1. check copy validity */
val result: RemoteOperationResult<Any>

if (targetParentPath.startsWith(srcPath)) {
result = RemoteOperationResult<Any>(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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2018-2023 Tobias Kaminsky <tobias@kaminsky.me>
* SPDX-FileCopyrightText: 2021 TSI-mc
* SPDX-FileCopyrightText: 2021-2025 TSI-mc <surinder.kumar@t-systems.com>
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-FileCopyrightText: 2017-2018 Andy Scherzinger <info@andy-scherzinger.de>
* SPDX-FileCopyrightText: 2015 ownCloud Inc.
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -123,6 +127,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;
Expand Down Expand Up @@ -758,6 +767,28 @@ private Pair<Target, RemoteOperation> 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;
Expand Down
Loading
Loading