Skip to content

Commit a42a89f

Browse files
committed
Albums functionality.
Signed-off-by: A117870935 <[email protected]>
1 parent d8a254f commit a42a89f

33 files changed

+2877
-33
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<!--
33
~ Nextcloud - Android Client
44
~
5-
~ SPDX-FileCopyrightText: 2024 TSI-mc <[email protected]>
5+
~ SPDX-FileCopyrightText: 2024-2025 TSI-mc <[email protected]>
66
~ SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
77
~ SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
88
-->
@@ -592,6 +592,9 @@
592592
android:launchMode="singleTop"
593593
android:theme="@style/Theme.ownCloud.Dialog.NoTitle"
594594
android:windowSoftInputMode="adjustResize" />
595+
<activity
596+
android:name=".ui.activity.AlbumsPickerActivity"
597+
android:exported="false" />
595598
<activity
596599
android:name=".ui.activity.ShareActivity"
597600
android:exported="false"

app/src/main/java/com/nextcloud/client/di/ComponentsModule.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
* Nextcloud - Android Client
33
*
4-
* SPDX-FileCopyrightText: 2024 TSI-mc <[email protected]>
4+
* SPDX-FileCopyrightText: 2024-2025 TSI-mc <[email protected]>
55
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <[email protected]>
66
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
77
*/
@@ -31,6 +31,7 @@
3131
import com.nextcloud.ui.ChooseStorageLocationDialogFragment;
3232
import com.nextcloud.ui.ImageDetailFragment;
3333
import com.nextcloud.ui.SetStatusDialogFragment;
34+
import com.nextcloud.ui.albumItemActions.AlbumItemActionsBottomSheet;
3435
import com.nextcloud.ui.composeActivity.ComposeActivity;
3536
import com.nextcloud.ui.fileactions.FileActionsBottomSheet;
3637
import com.nextcloud.ui.trashbinFileActions.TrashbinFileActionsBottomSheet;
@@ -81,6 +82,7 @@
8182
import com.owncloud.android.ui.dialog.ChooseTemplateDialogFragment;
8283
import com.owncloud.android.ui.dialog.ConfirmationDialogFragment;
8384
import com.owncloud.android.ui.dialog.ConflictsResolveDialog;
85+
import com.owncloud.android.ui.dialog.CreateAlbumDialogFragment;
8486
import com.owncloud.android.ui.dialog.CreateFolderDialogFragment;
8587
import com.owncloud.android.ui.dialog.ExpirationDatePickerDialogFragment;
8688
import com.owncloud.android.ui.dialog.IndeterminateProgressDialog;
@@ -114,6 +116,9 @@
114116
import com.owncloud.android.ui.fragment.OCFileListFragment;
115117
import com.owncloud.android.ui.fragment.SharedListFragment;
116118
import com.owncloud.android.ui.fragment.UnifiedSearchFragment;
119+
import com.owncloud.android.ui.fragment.albums.AlbumItemsFragment;
120+
import com.owncloud.android.ui.fragment.albums.AlbumsFragment;
121+
import com.owncloud.android.ui.activity.AlbumsPickerActivity;
117122
import com.owncloud.android.ui.fragment.contactsbackup.BackupFragment;
118123
import com.owncloud.android.ui.fragment.contactsbackup.BackupListFragment;
119124
import com.owncloud.android.ui.preview.FileDownloadFragment;
@@ -505,4 +510,19 @@ abstract class ComponentsModule {
505510

506511
@ContributesAndroidInjector
507512
abstract TermsOfServiceDialog termsOfServiceDialog();
513+
514+
@ContributesAndroidInjector
515+
abstract AlbumsPickerActivity albumsPickerActivity();
516+
517+
@ContributesAndroidInjector
518+
abstract CreateAlbumDialogFragment createAlbumDialogFragment();
519+
520+
@ContributesAndroidInjector
521+
abstract AlbumsFragment albumsFragment();
522+
523+
@ContributesAndroidInjector
524+
abstract AlbumItemsFragment albumItemsFragment();
525+
526+
@ContributesAndroidInjector
527+
abstract AlbumItemActionsBottomSheet albumItemActionsBottomSheet();
508528
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2025 TSI-mc <[email protected]>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
package com.nextcloud.ui.albumItemActions
9+
10+
import androidx.annotation.DrawableRes
11+
import androidx.annotation.IdRes
12+
import androidx.annotation.StringRes
13+
import com.owncloud.android.R
14+
15+
enum class AlbumItemAction(@IdRes val id: Int, @StringRes val title: Int, @DrawableRes val icon: Int? = null) {
16+
RENAME_ALBUM(R.id.action_rename_file, R.string.album_rename, R.drawable.ic_edit),
17+
DELETE_ALBUM(R.id.action_delete, R.string.album_delete, R.drawable.ic_delete);
18+
19+
companion object {
20+
/**
21+
* All file actions, in the order they should be displayed
22+
*/
23+
@JvmField
24+
val SORTED_VALUES = listOf(
25+
RENAME_ALBUM,
26+
DELETE_ALBUM,
27+
)
28+
}
29+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2025 TSI-mc <[email protected]>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
package com.nextcloud.ui.albumItemActions
9+
10+
import android.os.Bundle
11+
import android.view.LayoutInflater
12+
import android.view.View
13+
import android.view.ViewGroup
14+
import androidx.annotation.IdRes
15+
import androidx.appcompat.content.res.AppCompatResources
16+
import androidx.core.os.bundleOf
17+
import androidx.core.view.isEmpty
18+
import androidx.fragment.app.FragmentManager
19+
import androidx.fragment.app.setFragmentResult
20+
import androidx.lifecycle.LifecycleOwner
21+
import com.google.android.material.bottomsheet.BottomSheetBehavior
22+
import com.google.android.material.bottomsheet.BottomSheetDialog
23+
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
24+
import com.nextcloud.android.common.ui.theme.utils.ColorRole
25+
import com.nextcloud.client.di.Injectable
26+
import com.owncloud.android.databinding.FileActionsBottomSheetBinding
27+
import com.owncloud.android.databinding.FileActionsBottomSheetItemBinding
28+
import com.owncloud.android.utils.theme.ViewThemeUtils
29+
import javax.inject.Inject
30+
31+
class AlbumItemActionsBottomSheet : BottomSheetDialogFragment(), Injectable {
32+
33+
@Inject
34+
lateinit var viewThemeUtils: ViewThemeUtils
35+
36+
private var _binding: FileActionsBottomSheetBinding? = null
37+
val binding
38+
get() = _binding!!
39+
40+
fun interface ResultListener {
41+
fun onResult(@IdRes actionId: Int)
42+
}
43+
44+
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
45+
_binding = FileActionsBottomSheetBinding.inflate(inflater, container, false)
46+
47+
val bottomSheetDialog = dialog as BottomSheetDialog
48+
bottomSheetDialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED
49+
bottomSheetDialog.behavior.skipCollapsed = true
50+
51+
viewThemeUtils.platform.colorViewBackground(binding.bottomSheet, ColorRole.SURFACE)
52+
53+
return binding.root
54+
}
55+
56+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
57+
super.onViewCreated(view, savedInstanceState)
58+
binding.bottomSheetHeader.visibility = View.GONE
59+
binding.bottomSheetLoading.visibility = View.GONE
60+
displayActions()
61+
}
62+
63+
override fun onDestroyView() {
64+
super.onDestroyView()
65+
_binding = null
66+
}
67+
68+
fun setResultListener(
69+
fragmentManager: FragmentManager,
70+
lifecycleOwner: LifecycleOwner,
71+
listener: ResultListener
72+
): AlbumItemActionsBottomSheet {
73+
fragmentManager.setFragmentResultListener(REQUEST_KEY, lifecycleOwner) { _, result ->
74+
@IdRes val actionId = result.getInt(RESULT_KEY_ACTION_ID, -1)
75+
if (actionId != -1) {
76+
listener.onResult(actionId)
77+
}
78+
}
79+
return this
80+
}
81+
82+
private fun displayActions() {
83+
if (binding.fileActionsList.isEmpty()) {
84+
AlbumItemAction.SORTED_VALUES.forEach { action ->
85+
val view = inflateActionView(action)
86+
binding.fileActionsList.addView(view)
87+
}
88+
}
89+
}
90+
91+
private fun inflateActionView(action: AlbumItemAction): View {
92+
val itemBinding = FileActionsBottomSheetItemBinding.inflate(layoutInflater, binding.fileActionsList, false)
93+
.apply {
94+
root.setOnClickListener {
95+
dispatchActionClick(action.id)
96+
}
97+
text.setText(action.title)
98+
if (action.icon != null) {
99+
val drawable =
100+
viewThemeUtils.platform.tintDrawable(
101+
requireContext(),
102+
AppCompatResources.getDrawable(requireContext(), action.icon)!!
103+
)
104+
icon.setImageDrawable(drawable)
105+
}
106+
}
107+
return itemBinding.root
108+
}
109+
110+
private fun dispatchActionClick(id: Int?) {
111+
if (id != null) {
112+
setFragmentResult(REQUEST_KEY, bundleOf(RESULT_KEY_ACTION_ID to id))
113+
parentFragmentManager.clearFragmentResultListener(REQUEST_KEY)
114+
dismiss()
115+
}
116+
}
117+
118+
companion object {
119+
private const val REQUEST_KEY = "REQUEST_KEY_ACTION"
120+
private const val RESULT_KEY_ACTION_ID = "RESULT_KEY_ACTION_ID"
121+
122+
@JvmStatic
123+
fun newInstance(): AlbumItemActionsBottomSheet {
124+
return AlbumItemActionsBottomSheet()
125+
}
126+
}
127+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2025 TSI-mc <[email protected]>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
package com.owncloud.android.operations.albums;
9+
10+
import com.owncloud.android.datamodel.FileDataStorageManager;
11+
import com.owncloud.android.datamodel.OCFile;
12+
import com.owncloud.android.lib.common.OwnCloudClient;
13+
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
14+
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
15+
import com.owncloud.android.lib.resources.albums.CopyFileToAlbumRemoteOperation;
16+
import com.owncloud.android.operations.UploadFileOperation;
17+
import com.owncloud.android.operations.common.SyncOperation;
18+
19+
/**
20+
* Operation copying an {@link OCFile} to a different folder.
21+
*
22+
* @author David A. Velasco
23+
*/
24+
public class CopyFileToAlbumOperation extends SyncOperation {
25+
26+
private final String srcPath;
27+
private String targetParentPath;
28+
29+
/**
30+
* Constructor
31+
*
32+
* @param srcPath Remote path of the {@link OCFile} to move.
33+
* @param targetParentPath Path to the folder where the file will be copied into.
34+
*/
35+
public CopyFileToAlbumOperation(String srcPath, String targetParentPath, FileDataStorageManager storageManager) {
36+
super(storageManager);
37+
38+
this.srcPath = srcPath;
39+
this.targetParentPath = targetParentPath;
40+
if (!this.targetParentPath.endsWith(OCFile.PATH_SEPARATOR)) {
41+
this.targetParentPath += OCFile.PATH_SEPARATOR;
42+
}
43+
}
44+
45+
/**
46+
* Performs the operation.
47+
*
48+
* @param client Client object to communicate with the remote ownCloud server.
49+
*/
50+
@Override
51+
protected RemoteOperationResult run(OwnCloudClient client) {
52+
/// 1. check copy validity
53+
if (targetParentPath.startsWith(srcPath)) {
54+
return new RemoteOperationResult(ResultCode.INVALID_COPY_INTO_DESCENDANT);
55+
}
56+
OCFile file = getStorageManager().getFileByPath(srcPath);
57+
if (file == null) {
58+
return new RemoteOperationResult(ResultCode.FILE_NOT_FOUND);
59+
}
60+
61+
/// 2. remote copy
62+
String targetPath = targetParentPath + file.getFileName();
63+
if (file.isFolder()) {
64+
targetPath += OCFile.PATH_SEPARATOR;
65+
}
66+
67+
// auto rename, to allow copy
68+
if (targetPath.equals(srcPath)) {
69+
if (file.isFolder()) {
70+
targetPath = targetParentPath + file.getFileName();
71+
}
72+
targetPath = UploadFileOperation.getNewAvailableRemotePath(client, targetPath, null, false);
73+
74+
if (file.isFolder()) {
75+
targetPath += OCFile.PATH_SEPARATOR;
76+
}
77+
}
78+
79+
RemoteOperationResult result = new CopyFileToAlbumRemoteOperation(srcPath, targetPath).execute(client);
80+
81+
/// 3. local copy
82+
if (result.isSuccess()) {
83+
getStorageManager().copyLocalFile(file, targetPath);
84+
}
85+
// TODO handle ResultCode.PARTIAL_COPY_DONE in client Activity, for the moment
86+
87+
return result;
88+
}
89+
}

app/src/main/java/com/owncloud/android/services/OperationsService.java

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Nextcloud - Android Client
33
*
44
* SPDX-FileCopyrightText: 2018-2023 Tobias Kaminsky <[email protected]>
5-
* SPDX-FileCopyrightText: 2021 TSI-mc
5+
* SPDX-FileCopyrightText: 2021-2025 TSI-mc <[email protected]>
66
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <[email protected]>
77
* SPDX-FileCopyrightText: 2017-2018 Andy Scherzinger <[email protected]>
88
* SPDX-FileCopyrightText: 2015 ownCloud Inc.
@@ -42,6 +42,9 @@
4242
import com.owncloud.android.lib.common.operations.RemoteOperation;
4343
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
4444
import com.owncloud.android.lib.common.utils.Log_OC;
45+
import com.owncloud.android.lib.resources.albums.CreateNewAlbumRemoteOperation;
46+
import com.owncloud.android.lib.resources.albums.RemoveAlbumRemoteOperation;
47+
import com.owncloud.android.lib.resources.albums.RenameAlbumRemoteOperation;
4548
import com.owncloud.android.lib.resources.files.RestoreFileVersionRemoteOperation;
4649
import com.owncloud.android.lib.resources.files.model.FileVersion;
4750
import com.owncloud.android.lib.resources.shares.OCShare;
@@ -64,6 +67,7 @@
6467
import com.owncloud.android.operations.UpdateShareInfoOperation;
6568
import com.owncloud.android.operations.UpdateSharePermissionsOperation;
6669
import com.owncloud.android.operations.UpdateShareViaLinkOperation;
70+
import com.owncloud.android.operations.albums.CopyFileToAlbumOperation;
6771

6872
import java.io.IOException;
6973
import java.util.Optional;
@@ -122,6 +126,11 @@ public class OperationsService extends Service {
122126
public static final String ACTION_CHECK_CURRENT_CREDENTIALS = "CHECK_CURRENT_CREDENTIALS";
123127
public static final String ACTION_RESTORE_VERSION = "RESTORE_VERSION";
124128
public static final String ACTION_UPDATE_FILES_DOWNLOAD_LIMIT = "UPDATE_FILES_DOWNLOAD_LIMIT";
129+
public static final String ACTION_CREATE_ALBUM = "CREATE_ALBUM";
130+
public static final String EXTRA_ALBUM_NAME = "ALBUM_NAME";
131+
public static final String ACTION_ALBUM_COPY_FILE = "ALBUM_COPY_FILE";
132+
public static final String ACTION_RENAME_ALBUM = "RENAME_ALBUM";
133+
public static final String ACTION_REMOVE_ALBUM = "REMOVE_ALBUM";
125134

126135
private ServiceHandler mOperationsHandler;
127136
private OperationsServiceBinder mOperationsBinder;
@@ -746,6 +755,28 @@ private Pair<Target, RemoteOperation> newOperation(Intent operationIntent) {
746755
}
747756
break;
748757

758+
case ACTION_CREATE_ALBUM:
759+
String albumName = operationIntent.getStringExtra(EXTRA_ALBUM_NAME);
760+
operation = new CreateNewAlbumRemoteOperation(albumName);
761+
break;
762+
763+
case ACTION_ALBUM_COPY_FILE:
764+
remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
765+
newParentPath = operationIntent.getStringExtra(EXTRA_NEW_PARENT_PATH);
766+
operation = new CopyFileToAlbumOperation(remotePath, newParentPath, fileDataStorageManager);
767+
break;
768+
769+
case ACTION_RENAME_ALBUM:
770+
remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
771+
String newAlbumName = operationIntent.getStringExtra(EXTRA_NEWNAME);
772+
operation = new RenameAlbumRemoteOperation(remotePath, newAlbumName);
773+
break;
774+
775+
case ACTION_REMOVE_ALBUM:
776+
String albumNameToRemove = operationIntent.getStringExtra(EXTRA_ALBUM_NAME);
777+
operation = new RemoveAlbumRemoteOperation(albumNameToRemove);
778+
break;
779+
749780
default:
750781
// do nothing
751782
break;

0 commit comments

Comments
 (0)