Skip to content

Albums Functionality #330

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

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
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
20 changes: 20 additions & 0 deletions app/src/main/java/com/nextcloud/client/di/ComponentsModule.java
Original file line number Diff line number Diff line change
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,28 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 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: 2024 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
@@ -0,0 +1,161 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2021 Tobias Kaminsky <tobias@kaminsky.me>
* SPDX-FileCopyrightText: 2019 Andy Scherzinger <info@andy-scherzinger.de>
* SPDX-FileCopyrightText: 2012-2014 ownCloud Inc.
* SPDX-FileCopyrightText: 2014 Jorge Antonio Diaz-Benito Soriano <jorge.diazbenitosoriano@gmail.com>
* SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only)
*/
package com.owncloud.android.operations.albums;

import android.util.Log;

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.network.WebdavUtils;
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
import com.owncloud.android.operations.UploadFileOperation;
import com.owncloud.android.operations.common.SyncOperation;

import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.MultiStatusResponse;
import org.apache.jackrabbit.webdav.Status;
import org.apache.jackrabbit.webdav.client.methods.CopyMethod;

import java.io.IOException;

/**
* Operation copying an {@link OCFile} to a different folder.
*
* @author David A. Velasco
*/
public class CopyFileToAlbumOperation extends SyncOperation {
private static final String TAG = CopyFileToAlbumOperation.class.getSimpleName();

private final String srcPath;
private String targetParentPath;

/**
* Constructor
*
* @param srcPath Remote path of the {@link OCFile} to move.
* @param targetParentPath Path to the folder where the file will be copied into.
*/
public CopyFileToAlbumOperation(String srcPath, String targetParentPath, FileDataStorageManager storageManager) {
super(storageManager);

this.srcPath = srcPath;
this.targetParentPath = targetParentPath;
if (!this.targetParentPath.endsWith(OCFile.PATH_SEPARATOR)) {
this.targetParentPath += OCFile.PATH_SEPARATOR;
}
}

/**
* Performs the operation.
*
* @param client Client object to communicate with the remote ownCloud server.
*/
@Override
protected RemoteOperationResult run(OwnCloudClient client) {
/// 1. check copy validity
if (targetParentPath.startsWith(srcPath)) {
return new RemoteOperationResult(ResultCode.INVALID_COPY_INTO_DESCENDANT);
}
OCFile file = getStorageManager().getFileByPath(srcPath);
if (file == null) {
return new RemoteOperationResult(ResultCode.FILE_NOT_FOUND);
}

/// 2. remote copy
String targetPath = targetParentPath + file.getFileName();
if (file.isFolder()) {
targetPath += OCFile.PATH_SEPARATOR;
}

// auto rename, to allow copy
if (targetPath.equals(srcPath)) {
if (file.isFolder()) {
targetPath = targetParentPath + file.getFileName();
}
targetPath = UploadFileOperation.getNewAvailableRemotePath(client, targetPath, null, false);

if (file.isFolder()) {
targetPath += OCFile.PATH_SEPARATOR;
}
}

RemoteOperationResult result = performCopyOperation(targetPath, client);

/// 3. local copy
if (result.isSuccess()) {
getStorageManager().copyLocalFile(file, targetPath);
}
// TODO handle ResultCode.PARTIAL_COPY_DONE in client Activity, for the moment

return result;
}

private RemoteOperationResult performCopyOperation(String targetRemotePath, OwnCloudClient client) {
if (targetRemotePath.equals(this.srcPath)) {
return new RemoteOperationResult(ResultCode.OK);
} else if (targetRemotePath.startsWith(this.srcPath)) {
return new RemoteOperationResult(ResultCode.INVALID_COPY_INTO_DESCENDANT);
} else {
CopyMethod copyMethod = null;
RemoteOperationResult result;

try {
copyMethod = new CopyMethod(client.getFilesDavUri(this.srcPath), client.getBaseUri() + "/remote.php/dav/photos/" + client.getUserId() + "/albums" + WebdavUtils.encodePath(targetRemotePath), false);
int status = client.executeMethod(copyMethod);
if (status == 207) {
result = this.processPartialError(copyMethod);
} else if (status == 412) {
result = new RemoteOperationResult(ResultCode.INVALID_OVERWRITE);
client.exhaustResponse(copyMethod.getResponseBodyAsStream());
} else {
result = new RemoteOperationResult(this.isSuccess(status), copyMethod);
client.exhaustResponse(copyMethod.getResponseBodyAsStream());
}

Log.i(TAG, "Copy " + this.srcPath + " to " + targetRemotePath + ": " + result.getLogMessage());
} catch (Exception e) {
result = new RemoteOperationResult(e);
Log.e(TAG, "Copy " + this.srcPath + " to " + targetRemotePath + ": " + result.getLogMessage(), e);
} finally {
if (copyMethod != null) {
copyMethod.releaseConnection();
}

}

return result;
}
}

private RemoteOperationResult processPartialError(CopyMethod copyMethod) throws IOException, DavException {
MultiStatusResponse[] responses = copyMethod.getResponseBodyAsMultiStatus().getResponses();
boolean failFound = false;

for (int i = 0; i < responses.length && !failFound; ++i) {
Status[] status = responses[i].getStatus();
failFound = status != null && status.length > 0 && status[0].getStatusCode() > 299;
}

RemoteOperationResult result;
if (failFound) {
result = new RemoteOperationResult(ResultCode.PARTIAL_COPY_DONE);
} else {
result = new RemoteOperationResult(true, copyMethod);
}

return result;
}

protected boolean isSuccess(int status) {
return status == 201 || status == 204;
}
}
Loading