Skip to content

Commit 20feb13

Browse files
committed
Albums functionality
1 parent 93fd21e commit 20feb13

File tree

54 files changed

+5374
-36
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+5374
-36
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,9 @@
598598
android:launchMode="singleTop"
599599
android:theme="@style/Theme.ownCloud.Dialog.NoTitle"
600600
android:windowSoftInputMode="adjustResize" />
601+
<activity
602+
android:name=".ui.activity.AlbumsPickerActivity"
603+
android:exported="false" />
601604
<activity
602605
android:name=".ui.activity.ShareActivity"
603606
android:exported="false"

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import com.nextcloud.ui.ImageDetailFragment;
3333
import com.nextcloud.ui.SetOnlineStatusBottomSheet;
3434
import com.nextcloud.ui.SetStatusMessageBottomSheet;
35+
import com.nextcloud.ui.albumItemActions.AlbumItemActionsBottomSheet;
3536
import com.nextcloud.ui.composeActivity.ComposeActivity;
3637
import com.nextcloud.ui.fileactions.FileActionsBottomSheet;
3738
import com.nextcloud.ui.trashbinFileActions.TrashbinFileActionsBottomSheet;
@@ -82,6 +83,7 @@
8283
import com.owncloud.android.ui.dialog.ChooseTemplateDialogFragment;
8384
import com.owncloud.android.ui.dialog.ConfirmationDialogFragment;
8485
import com.owncloud.android.ui.dialog.ConflictsResolveDialog;
86+
import com.owncloud.android.ui.dialog.CreateAlbumDialogFragment;
8587
import com.owncloud.android.ui.dialog.CreateFolderDialogFragment;
8688
import com.owncloud.android.ui.dialog.ExpirationDatePickerDialogFragment;
8789
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 SetStatusMessageBottomSheet setStatusMessageBottomSheet();
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 <surinder.kumar@t-systems.com>
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: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2025 TSI-mc <surinder.kumar@t-systems.com>
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.core.os.bundleOf
16+
import androidx.core.view.isEmpty
17+
import androidx.fragment.app.FragmentManager
18+
import androidx.fragment.app.setFragmentResult
19+
import androidx.lifecycle.LifecycleOwner
20+
import com.google.android.material.bottomsheet.BottomSheetBehavior
21+
import com.google.android.material.bottomsheet.BottomSheetDialog
22+
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
23+
import com.nextcloud.client.di.Injectable
24+
import com.owncloud.android.databinding.FileActionsBottomSheetBinding
25+
import com.owncloud.android.databinding.FileActionsBottomSheetItemBinding
26+
import com.owncloud.android.utils.theme.ViewThemeUtils
27+
import javax.inject.Inject
28+
29+
class AlbumItemActionsBottomSheet : BottomSheetDialogFragment(), Injectable {
30+
31+
@Inject
32+
lateinit var viewThemeUtils: ViewThemeUtils
33+
34+
private var _binding: FileActionsBottomSheetBinding? = null
35+
val binding
36+
get() = _binding!!
37+
38+
fun interface ResultListener {
39+
fun onResult(@IdRes actionId: Int)
40+
}
41+
42+
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
43+
_binding = FileActionsBottomSheetBinding.inflate(inflater, container, false)
44+
45+
val bottomSheetDialog = dialog as BottomSheetDialog
46+
bottomSheetDialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED
47+
bottomSheetDialog.behavior.skipCollapsed = true
48+
return binding.root
49+
}
50+
51+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
52+
super.onViewCreated(view, savedInstanceState)
53+
binding.bottomSheetHeader.visibility = View.GONE
54+
binding.bottomSheetLoading.visibility = View.GONE
55+
displayActions()
56+
}
57+
58+
override fun onDestroyView() {
59+
super.onDestroyView()
60+
_binding = null
61+
}
62+
63+
fun setResultListener(
64+
fragmentManager: FragmentManager,
65+
lifecycleOwner: LifecycleOwner,
66+
listener: ResultListener
67+
): AlbumItemActionsBottomSheet {
68+
fragmentManager.setFragmentResultListener(REQUEST_KEY, lifecycleOwner) { _, result ->
69+
@IdRes val actionId = result.getInt(RESULT_KEY_ACTION_ID, -1)
70+
if (actionId != -1) {
71+
listener.onResult(actionId)
72+
}
73+
}
74+
return this
75+
}
76+
77+
private fun displayActions() {
78+
if (binding.fileActionsList.isEmpty()) {
79+
AlbumItemAction.SORTED_VALUES.forEach { action ->
80+
val view = inflateActionView(action)
81+
binding.fileActionsList.addView(view)
82+
}
83+
}
84+
}
85+
86+
private fun inflateActionView(action: AlbumItemAction): View {
87+
val itemBinding = FileActionsBottomSheetItemBinding.inflate(layoutInflater, binding.fileActionsList, false)
88+
.apply {
89+
root.setOnClickListener {
90+
dispatchActionClick(action.id)
91+
}
92+
text.setText(action.title)
93+
if (action.icon != null) {
94+
icon.setImageResource(action.icon)
95+
}
96+
}
97+
return itemBinding.root
98+
}
99+
100+
private fun dispatchActionClick(id: Int?) {
101+
if (id != null) {
102+
setFragmentResult(REQUEST_KEY, bundleOf(RESULT_KEY_ACTION_ID to id))
103+
parentFragmentManager.clearFragmentResultListener(REQUEST_KEY)
104+
dismiss()
105+
}
106+
}
107+
108+
companion object {
109+
private const val REQUEST_KEY = "REQUEST_KEY_ACTION"
110+
private const val RESULT_KEY_ACTION_ID = "RESULT_KEY_ACTION_ID"
111+
112+
@JvmStatic
113+
fun newInstance(): AlbumItemActionsBottomSheet {
114+
return AlbumItemActionsBottomSheet()
115+
}
116+
}
117+
}

app/src/main/java/com/owncloud/android/datamodel/VirtualFolderType.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@
1212
* Type for virtual folders
1313
*/
1414
public enum VirtualFolderType {
15-
FAVORITE, GALLERY, NONE
15+
FAVORITE, GALLERY, ALBUM, NONE
1616
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2025 TSI-mc <surinder.kumar@t-systems.com>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
6+
*/
7+
package com.owncloud.android.operations
8+
9+
import android.content.Context
10+
import com.nextcloud.client.account.User
11+
import com.owncloud.android.MainApp
12+
import com.owncloud.android.datamodel.FileDataStorageManager
13+
import com.owncloud.android.datamodel.OCFile
14+
import com.owncloud.android.lib.common.OwnCloudClient
15+
import com.owncloud.android.lib.common.operations.RemoteOperation
16+
import com.owncloud.android.lib.common.operations.RemoteOperationResult
17+
import com.owncloud.android.lib.common.utils.Log_OC
18+
import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation
19+
import com.owncloud.android.lib.resources.files.SearchRemoteOperation
20+
import com.owncloud.android.lib.resources.files.model.RemoteFile
21+
import com.owncloud.android.operations.common.SyncOperation
22+
import com.owncloud.android.utils.FileStorageUtils
23+
24+
/**
25+
* fetch OCFile meta data if not present in local db
26+
*
27+
* @see com.owncloud.android.ui.asynctasks.FetchRemoteFileTask reference for this operation
28+
*
29+
* @param ocFile file for which metadata has to retrieve
30+
* @param removeFileFromDb if you want to remove ocFile from local db to avoid duplicate entries for same fileId
31+
*/
32+
class FetchRemoteFileOperation(
33+
private val context: Context,
34+
private val user: User,
35+
private val ocFile: OCFile,
36+
private val removeFileFromDb: Boolean = false,
37+
storageManager: FileDataStorageManager,
38+
) : SyncOperation(storageManager) {
39+
40+
@Deprecated("Deprecated in Java")
41+
override fun run(client: OwnCloudClient?): RemoteOperationResult<*>? {
42+
val searchRemoteOperation = SearchRemoteOperation(
43+
ocFile.localId.toString(),
44+
SearchRemoteOperation.SearchType.FILE_ID_SEARCH,
45+
false,
46+
storageManager.getCapability(user)
47+
)
48+
val remoteOperationResult: RemoteOperationResult<List<RemoteFile>> =
49+
searchRemoteOperation.execute(user, context)
50+
51+
if (remoteOperationResult.isSuccess && remoteOperationResult.resultData != null) {
52+
if (remoteOperationResult.resultData.isEmpty()) {
53+
Log_OC.e(TAG, "No remote file found with id: ${ocFile.localId}.")
54+
return remoteOperationResult
55+
}
56+
val remotePath = (remoteOperationResult.resultData[0]).remotePath
57+
58+
val operation = ReadFileRemoteOperation(remotePath)
59+
val result = operation.execute(user, context)
60+
61+
if (!result.isSuccess) {
62+
val exception = result.exception
63+
val message =
64+
"Fetching file " + remotePath + " fails with: " + result.getLogMessage(MainApp.getAppContext())
65+
Log_OC.e(TAG, exception?.message ?: message)
66+
67+
return result
68+
}
69+
70+
val remoteFile = result.data[0] as RemoteFile
71+
72+
// remove file from local db
73+
if (removeFileFromDb) {
74+
storageManager.removeFile(ocFile, true, true)
75+
}
76+
77+
var ocFile = FileStorageUtils.fillOCFile(remoteFile)
78+
FileStorageUtils.searchForLocalFileInDefaultPath(ocFile, user.accountName)
79+
ocFile = storageManager.saveFileWithParent(ocFile, context)
80+
81+
// also sync folder content
82+
val toSync: OCFile? = if (ocFile?.isFolder == true) {
83+
ocFile
84+
} else {
85+
ocFile?.parentId?.let { storageManager.getFileById(it) }
86+
}
87+
88+
val currentSyncTime = System.currentTimeMillis()
89+
val refreshFolderOperation: RemoteOperation<Any> = RefreshFolderOperation(
90+
toSync,
91+
currentSyncTime,
92+
true,
93+
true,
94+
storageManager,
95+
user,
96+
context
97+
)
98+
val refreshOperationResult = refreshFolderOperation.execute(user, context)
99+
100+
// set the fetched ocFile to resultData to be handled at ui end
101+
refreshOperationResult.resultData = ocFile
102+
103+
return refreshOperationResult
104+
}
105+
return remoteOperationResult
106+
}
107+
108+
companion object {
109+
private val TAG = FetchRemoteFileOperation::class.java.simpleName
110+
}
111+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2025 TSI-mc <surinder.kumar@t-systems.com>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
package com.owncloud.android.operations.albums
8+
9+
import com.owncloud.android.datamodel.FileDataStorageManager
10+
import com.owncloud.android.datamodel.OCFile
11+
import com.owncloud.android.lib.common.OwnCloudClient
12+
import com.owncloud.android.lib.common.operations.RemoteOperationResult
13+
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode
14+
import com.owncloud.android.operations.UploadFileOperation
15+
import com.owncloud.android.operations.common.SyncOperation
16+
17+
/**
18+
* Constructor
19+
*
20+
* @param srcPath Remote path of the [OCFile] to move.
21+
* @param targetParentPath Path to the folder where the file will be copied into.
22+
*/
23+
class CopyFileToAlbumOperation(
24+
private val srcPath: String,
25+
private var targetParentPath: String,
26+
storageManager: FileDataStorageManager
27+
) :
28+
SyncOperation(storageManager) {
29+
init {
30+
if (!targetParentPath.endsWith(OCFile.PATH_SEPARATOR)) {
31+
this.targetParentPath += OCFile.PATH_SEPARATOR
32+
}
33+
}
34+
35+
/**
36+
* Performs the operation.
37+
*
38+
* @param client Client object to communicate with the remote ownCloud server.
39+
*/
40+
@Deprecated("Deprecated in Java")
41+
@Suppress("NestedBlockDepth")
42+
override fun run(client: OwnCloudClient): RemoteOperationResult<Any> {
43+
/** 1. check copy validity */
44+
val result: RemoteOperationResult<Any>
45+
46+
if (targetParentPath.startsWith(srcPath)) {
47+
result = RemoteOperationResult<Any>(ResultCode.INVALID_COPY_INTO_DESCENDANT)
48+
} else {
49+
val file = storageManager.getFileByPath(srcPath)
50+
if (file == null) {
51+
result = RemoteOperationResult(ResultCode.FILE_NOT_FOUND)
52+
} else {
53+
/** 2. remote copy */
54+
var targetPath = "$targetParentPath${file.fileName}"
55+
if (file.isFolder) {
56+
targetPath += OCFile.PATH_SEPARATOR
57+
}
58+
59+
// auto rename, to allow copy
60+
if (targetPath == srcPath) {
61+
if (file.isFolder) {
62+
targetPath = "$targetParentPath${file.fileName}"
63+
}
64+
targetPath = UploadFileOperation.getNewAvailableRemotePath(client, targetPath, null, false)
65+
66+
if (file.isFolder) {
67+
targetPath += OCFile.PATH_SEPARATOR
68+
}
69+
}
70+
71+
result = CopyFileToAlbumRemoteOperation(srcPath, targetPath).execute(client)
72+
}
73+
}
74+
return result
75+
}
76+
}

0 commit comments

Comments
 (0)