From 52c95896e59e51d2d81e017127e1cb23a66f9027 Mon Sep 17 00:00:00 2001 From: joragua Date: Thu, 5 Mar 2026 12:07:29 +0100 Subject: [PATCH 1/5] feat: add "+" button to create space public links only for space managers --- .../spaces/members/SpaceMembersFragment.kt | 14 +++++++++++--- .../src/main/res/layout/members_fragment.xml | 15 +++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersFragment.kt index 4ea5d250ba7..c96166ce202 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersFragment.kt @@ -215,9 +215,7 @@ class SpaceMembersFragment : Fragment(), SpaceMembersAdapter.SpaceMembersAdapter when (val uiResult = event.peekContent()) { is UIResult.Success -> { uiResult.data?.let { spacePermissions -> - binding.addMemberButton.isVisible = DRIVES_CREATE_PERMISSION in spacePermissions - canRemoveMembers = DRIVES_DELETE_PERMISSION in spacePermissions - canEditMembers = DRIVES_UPDATE_PERMISSION in spacePermissions + checkPermissions(spacePermissions) spaceMembersAdapter.setSpaceMembers(spaceMembers, roles, canRemoveMembers, canEditMembers, numberOfManagers) } } @@ -270,6 +268,16 @@ class SpaceMembersFragment : Fragment(), SpaceMembersAdapter.SpaceMembersAdapter } } + private fun checkPermissions(spacePermissions: List) { + binding.apply { + val hasCreatePermission = DRIVES_CREATE_PERMISSION in spacePermissions + addMemberButton.isVisible = hasCreatePermission + addPublicLinkButton.isVisible = hasCreatePermission + } + canRemoveMembers = DRIVES_DELETE_PERMISSION in spacePermissions + canEditMembers = DRIVES_UPDATE_PERMISSION in spacePermissions + } + private fun showOrHideEmptyView(hasLinks: Boolean) { binding.apply { publicLinksRecyclerView.isVisible = hasLinks diff --git a/owncloudApp/src/main/res/layout/members_fragment.xml b/owncloudApp/src/main/res/layout/members_fragment.xml index 9331537adf5..f56c56796d8 100644 --- a/owncloudApp/src/main/res/layout/members_fragment.xml +++ b/owncloudApp/src/main/res/layout/members_fragment.xml @@ -103,6 +103,21 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"/> + + Date: Wed, 25 Mar 2026 12:18:11 +0100 Subject: [PATCH 2/5] feat: add layout for creating a public link over a space --- .../dependecyinjection/ViewModelModule.kt | 4 +- .../spaces/links/AddPublicLinkFragment.kt | 268 ++++++++++++++++++ .../spaces/links/SetPasswordDialogFragment.kt | 267 +++++++++++++++++ .../spaces/links/SpaceLinksViewModel.kt | 55 ++++ .../spaces/members/SpaceMembersActivity.kt | 12 + .../spaces/members/SpaceMembersFragment.kt | 5 + .../main/res/drawable/ic_secret_file_drop.xml | 10 + .../main/res/layout/add_member_fragment.xml | 2 +- .../res/layout/add_public_link_fragment.xml | 157 ++++++++++ ...te_item.xml => expiration_date_layout.xml} | 2 +- .../src/main/res/layout/password_layout.xml | 130 +++++++++ .../res/layout/public_link_permissions.xml | 211 ++++++++++++++ .../main/res/layout/set_password_dialog.xml | 267 +++++++++++++++++ owncloudApp/src/main/res/values/strings.xml | 13 + 14 files changed, 1400 insertions(+), 3 deletions(-) create mode 100644 owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/links/AddPublicLinkFragment.kt create mode 100644 owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/links/SetPasswordDialogFragment.kt create mode 100644 owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/links/SpaceLinksViewModel.kt create mode 100644 owncloudApp/src/main/res/drawable/ic_secret_file_drop.xml create mode 100644 owncloudApp/src/main/res/layout/add_public_link_fragment.xml rename owncloudApp/src/main/res/layout/{expiration_date_item.xml => expiration_date_layout.xml} (98%) create mode 100644 owncloudApp/src/main/res/layout/password_layout.xml create mode 100644 owncloudApp/src/main/res/layout/public_link_permissions.xml create mode 100644 owncloudApp/src/main/res/layout/set_password_dialog.xml diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt index 201c13e9b02..c9dee872bcb 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt @@ -7,7 +7,7 @@ * @author David Crespo Ríos * @author Jorge Aguado Recio * - * Copyright (C) 2025 ownCloud GmbH. + * Copyright (C) 2026 ownCloud GmbH. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -55,6 +55,7 @@ import com.owncloud.android.presentation.settings.more.SettingsMoreViewModel import com.owncloud.android.presentation.settings.security.SettingsSecurityViewModel import com.owncloud.android.presentation.sharing.ShareViewModel import com.owncloud.android.presentation.spaces.SpacesListViewModel +import com.owncloud.android.presentation.spaces.links.SpaceLinksViewModel import com.owncloud.android.presentation.spaces.members.SpaceMembersViewModel import com.owncloud.android.presentation.transfers.TransfersViewModel import com.owncloud.android.ui.ReceiveExternalFilesViewModel @@ -85,6 +86,7 @@ val viewModelModule = module { viewModelOf(::SettingsSecurityViewModel) viewModelOf(::SettingsVideoUploadsViewModel) viewModelOf(::SettingsViewModel) + viewModelOf(::SpaceLinksViewModel) viewModelOf(::SpaceMembersViewModel) viewModelOf(::FileOperationsViewModel) diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/links/AddPublicLinkFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/links/AddPublicLinkFragment.kt new file mode 100644 index 00000000000..afa7e6afe73 --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/links/AddPublicLinkFragment.kt @@ -0,0 +1,268 @@ +/** + * ownCloud Android client application + * + * @author Jorge Aguado Recio + * + * Copyright (C) 2026 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.presentation.spaces.links + +import android.app.DatePickerDialog +import android.icu.util.Calendar +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.RadioButton +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import com.owncloud.android.R +import com.owncloud.android.databinding.AddPublicLinkFragmentBinding +import com.owncloud.android.domain.capabilities.model.CapabilityBooleanType +import com.owncloud.android.domain.capabilities.model.OCCapability +import com.owncloud.android.domain.links.model.OCLinkType +import com.owncloud.android.extensions.collectLatestLifecycleFlow +import com.owncloud.android.presentation.capabilities.CapabilityViewModel +import com.owncloud.android.presentation.common.UIResult +import com.owncloud.android.utils.DisplayUtils +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koin.core.parameter.parametersOf +import timber.log.Timber +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.TimeZone + +class AddPublicLinkFragment: Fragment(), SetPasswordDialogFragment.SetPasswordListener { + private var _binding: AddPublicLinkFragmentBinding? = null + private val binding get() = _binding!! + + private val spaceLinksViewModel: SpaceLinksViewModel by viewModel() + private val capabilityViewModel: CapabilityViewModel by viewModel { + parametersOf( + requireArguments().getString(ARG_ACCOUNT_NAME) + ) + } + + private var capabilities: OCCapability? = null + private var isPasswordEnforced = true + private var hasPassword = false + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + _binding = AddPublicLinkFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + requireActivity().setTitle(R.string.public_link_create_title) + + binding.publicLinkPermissions.apply { + canViewPublicLinkRadioButton.tag = OCLinkType.CAN_VIEW + canEditPublicLinkRadioButton.tag = OCLinkType.CAN_EDIT + secretFileDropPublicLinkRadioButton.tag = OCLinkType.CREATE_ONLY + } + + collectLatestLifecycleFlow(spaceLinksViewModel.addPublicLinkUIState) { uiState -> + uiState?.let { + it.selectedExpirationDate?.let { expirationDate -> + binding.expirationDateLayout.expirationDateValue.apply { + visibility = View.VISIBLE + text = DisplayUtils.displayDateToHumanReadable(expirationDate) + } + } + + hasPassword = it.selectedPassword != null + it.selectedPermission?.let { + binding.optionsLayout.isVisible = true + binding.passwordLayout.apply { + passwordValue.isVisible = hasPassword + setPasswordButton.isVisible = !hasPassword && isPasswordEnforced + removePasswordButton.isVisible = hasPassword && isPasswordEnforced + setPasswordSwitch.isVisible = !isPasswordEnforced + setPasswordSwitch.isChecked = hasPassword + } + binding.createPublicLinkButton.isEnabled = isPasswordEnforced && hasPassword || !isPasswordEnforced + } + + bindDatePickerDialog(uiState.selectedExpirationDate) + + binding.expirationDateLayout.apply { + expirationDateLayout.setOnClickListener { + if (uiState.selectedExpirationDate != null) { + openDatePickerDialog(uiState.selectedExpirationDate) + } else { + expirationDateSwitch.isChecked = true + } + } + } + + binding.passwordLayout.apply { + passwordLayout.setOnClickListener { + if (!isPasswordEnforced){ + setPasswordSwitch.isChecked = true + } + showPasswordDialog(uiState.selectedPassword) + } + } + } + } + + capabilityViewModel.capabilities.observe(viewLifecycleOwner) { event-> + when (val uiResult = event.peekContent()) { + is UIResult.Success -> { + capabilities = uiResult.data + } + is UIResult.Loading -> { } + is UIResult.Error -> { + Timber.e(uiResult.error, "Failed to retrieve server capabilities") + } + } + } + + binding.publicLinkPermissions.apply { + canViewPublicLinkRadioButton.setOnClickListener { selectRadioButton(canViewPublicLinkRadioButton) } + canViewPublicLinkLayout.setOnClickListener { selectRadioButton(canViewPublicLinkRadioButton) } + canEditPublicLinkRadioButton.setOnClickListener { selectRadioButton(canEditPublicLinkRadioButton) } + canEditPublicLinkLayout.setOnClickListener { selectRadioButton(canEditPublicLinkRadioButton) } + secretFileDropPublicLinkRadioButton.setOnClickListener { selectRadioButton(secretFileDropPublicLinkRadioButton) } + secretFileDropPublicLinkLayout.setOnClickListener { selectRadioButton(secretFileDropPublicLinkRadioButton) } + } + + binding.passwordLayout.apply { + setPasswordButton.setOnClickListener { + showPasswordDialog() + } + removePasswordButton.setOnClickListener { + removePassword() + } + setPasswordSwitch.setOnClickListener { + if (setPasswordSwitch.isChecked) showPasswordDialog() else removePassword() + } + } + } + + override fun onCancelPassword() { + if (!isPasswordEnforced && !hasPassword) { + binding.passwordLayout.setPasswordSwitch.isChecked = false + } + } + + override fun onSetPassword(password: String) { + spaceLinksViewModel.onPasswordSelected(password) + } + + private fun selectRadioButton(selectedRadioButton: RadioButton) { + binding.publicLinkPermissions.apply { + canViewPublicLinkRadioButton.isChecked = false + canEditPublicLinkRadioButton.isChecked = false + secretFileDropPublicLinkRadioButton.isChecked = false + selectedRadioButton.isChecked = true + } + val selectedPermission = selectedRadioButton.tag as OCLinkType + checkPasswordEnforced(selectedPermission) + spaceLinksViewModel.onPermissionSelected(selectedPermission) + } + + private fun checkPasswordEnforced(selectedPermission: OCLinkType) { + isPasswordEnforced = when (selectedPermission) { + OCLinkType.CAN_VIEW -> { + capabilities?.filesSharingPublicPasswordEnforcedReadOnly == CapabilityBooleanType.TRUE + } + OCLinkType.CAN_EDIT -> { + capabilities?.filesSharingPublicPasswordEnforcedReadWrite == CapabilityBooleanType.TRUE + } + OCLinkType.CREATE_ONLY -> { + capabilities?.filesSharingPublicPasswordEnforcedUploadOnly == CapabilityBooleanType.TRUE + } + else -> { + true + } + } + } + + private fun bindDatePickerDialog(expirationDate: String?) { + binding.expirationDateLayout.expirationDateSwitch.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) { + openDatePickerDialog(expirationDate) + } else { + binding.expirationDateLayout.expirationDateValue.visibility = View.GONE + spaceLinksViewModel.onExpirationDateSelected(null) + } + } + } + + private fun openDatePickerDialog(expirationDate: String?) { + val calendar = Calendar.getInstance() + val formatter = SimpleDateFormat(DisplayUtils.DATE_FORMAT_ISO, Locale.ROOT).apply { + timeZone = TimeZone.getTimeZone("UTC") + } + + expirationDate?.let { + calendar.time = formatter.parse(it) + } + + DatePickerDialog( + requireContext(), + { _, selectedYear, selectedMonth, selectedDay -> + calendar.set(selectedYear, selectedMonth, selectedDay, 23, 59, 59) + calendar.set(Calendar.MILLISECOND, 999) + val isoExpirationDate = formatter.format(calendar.time) + spaceLinksViewModel.onExpirationDateSelected(isoExpirationDate) + binding.expirationDateLayout.expirationDateValue.apply { + visibility = View.VISIBLE + text = DisplayUtils.displayDateToHumanReadable(isoExpirationDate) + } + }, + calendar.get(Calendar.YEAR), + calendar.get(Calendar.MONTH), + calendar.get(Calendar.DAY_OF_MONTH) + ).apply { + datePicker.minDate = Calendar.getInstance().timeInMillis + show() + setOnCancelListener { + if (expirationDate == null) { + binding.expirationDateLayout.expirationDateSwitch.isChecked = false + } + } + } + } + + private fun showPasswordDialog(password: String? = null) { + val accountName = requireArguments().getString(ARG_ACCOUNT_NAME) ?: return + val dialog = SetPasswordDialogFragment.newInstance(accountName, password, this) + dialog.show(parentFragmentManager, DIALOG_SET_PASSWORD) + } + + private fun removePassword() { + spaceLinksViewModel.onPasswordSelected(null) + } + + companion object { + private const val DIALOG_SET_PASSWORD = "DIALOG_SET_PASSWORD" + private const val ARG_ACCOUNT_NAME = "ARG_ACCOUNT_NAME" + + fun newInstance( + accountName: String + ): AddPublicLinkFragment { + val args = Bundle().apply { + putString(ARG_ACCOUNT_NAME, accountName) + } + return AddPublicLinkFragment().apply { + arguments = args + } + } + } +} diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/links/SetPasswordDialogFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/links/SetPasswordDialogFragment.kt new file mode 100644 index 00000000000..5a26c972d1a --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/links/SetPasswordDialogFragment.kt @@ -0,0 +1,267 @@ +/** + * ownCloud Android client application + * + * @author Jorge Aguado Recio + * + * Copyright (C) 2026 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.presentation.spaces.links + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.DialogInterface +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.appcompat.widget.AppCompatButton +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import androidx.core.widget.doOnTextChanged +import androidx.fragment.app.DialogFragment +import com.owncloud.android.R +import com.owncloud.android.databinding.SetPasswordDialogBinding +import com.owncloud.android.domain.capabilities.model.OCCapability +import com.owncloud.android.presentation.capabilities.CapabilityViewModel +import com.owncloud.android.presentation.common.UIResult +import com.owncloud.android.presentation.sharing.generatePassword +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koin.core.parameter.parametersOf +import timber.log.Timber + +class SetPasswordDialogFragment: DialogFragment() { + private var _binding: SetPasswordDialogBinding? = null + private val binding get() = _binding!! + + private val capabilityViewModel: CapabilityViewModel by viewModel { + parametersOf( + requireArguments().getString(ARG_ACCOUNT_NAME) + ) + } + + private lateinit var setPasswordListener: SetPasswordListener + + private var passwordPolicy: OCCapability.PasswordPolicy? = null + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + _binding = SetPasswordDialogBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + requireArguments().getString(ARG_PASSWORD)?.let { + binding.passwordValue.setText(it) + } + + capabilityViewModel.capabilities.observe(viewLifecycleOwner) { event-> + when (val uiResult = event.peekContent()) { + is UIResult.Success -> { + passwordPolicy = uiResult.data?.passwordPolicy + passwordPolicy?.let { + updatePasswordPolicyRequirements(binding.passwordValue.text.toString(), it) + } + } + is UIResult.Loading -> { } + is UIResult.Error -> { + Timber.e(uiResult.error, "Failed to retrieve server capabilities") + } + } + } + + binding.passwordValue.doOnTextChanged { text, _, _, _ -> + passwordPolicy?.let { + updatePasswordPolicyRequirements(text.toString(), it) + } + } + + binding.generatePasswordButton.setOnClickListener { + passwordPolicy?.let { passwordPolicy -> + binding.passwordValue.setText( + generatePassword( + minChars = passwordPolicy.minCharacters, + maxChars = passwordPolicy.maxCharacters, + minDigitsChars = passwordPolicy.minDigits, + minLowercaseChars = passwordPolicy.minLowercaseCharacters, + minUppercaseChars = passwordPolicy.minUppercaseCharacters, + minSpecialChars = passwordPolicy.minSpecialCharacters, + ) + ) + } + } + + binding.copyPasswordButton.setOnClickListener { + val clipboard = requireActivity().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText("Public link", binding.passwordValue.text.toString()) + clipboard.setPrimaryClip(clip) + } + + binding.cancelPasswordButton.setOnClickListener { + setPasswordListener.onCancelPassword() + dismiss() + } + + binding.setPasswordButton.setOnClickListener { + setPasswordListener.onSetPassword(binding.passwordValue.text.toString()) + dismiss() + } + } + + override fun onCancel(dialog: DialogInterface) { + super.onCancel(dialog) + setPasswordListener.onCancelPassword() + } + + private fun updatePasswordPolicyRequirements(password: String, passwordPolicy: OCCapability.PasswordPolicy) { + var hasMinCharacters = true + var hasMaxCharacters = true + var hasUpperCase = true + var hasLowerCase = true + var hasSpecialCharacter = true + var hasDigit = true + + passwordPolicy.minCharacters?.let { minCharacters -> + if (minCharacters > 0) { + hasMinCharacters = password.length >= minCharacters + updateRequirement( + hasRequirement = hasMinCharacters, + layout = binding.passwordPolicyMinCharacters, + textView = binding.passwordPolicyMinCharactersText, + textViewIcon = binding.passwordPolicyMinCharactersIcon, + text = getString(R.string.password_policy_min_characters, passwordPolicy.minCharacters) + ) + } + } + + passwordPolicy.maxCharacters?.let { maxCharacters -> + if (maxCharacters > 0) { + hasMaxCharacters = password.length <= maxCharacters + updateRequirement( + hasRequirement = hasMaxCharacters, + layout = binding.passwordPolicyMaxCharacters, + textView = binding.passwordPolicyMaxCharactersText, + textViewIcon = binding.passwordPolicyMaxCharactersIcon, + text = getString(R.string.password_policy_max_characters, passwordPolicy.maxCharacters) + ) + } + } + + passwordPolicy.minUppercaseCharacters?.let { minUppercaseCharacters -> + if (minUppercaseCharacters > 0) { + hasUpperCase = password.count { it.isUpperCase() } >= minUppercaseCharacters + updateRequirement( + hasRequirement = hasUpperCase, + layout = binding.passwordPolicyUpperCharacters, + textView = binding.passwordPolicyUpperCharactersText, + textViewIcon = binding.passwordPolicyUpperCharactersIcon, + text = getString(R.string.password_policy_uppercase_characters, passwordPolicy.minUppercaseCharacters) + ) + } + } + + passwordPolicy.minLowercaseCharacters?.let { minLowercaseCharacters -> + if (minLowercaseCharacters > 0) { + hasLowerCase = password.count { it.isLowerCase() } >= minLowercaseCharacters + updateRequirement( + hasRequirement = hasLowerCase, + layout = binding.passwordPolicyLowerCaseCharacters, + textView = binding.passwordPolicyLowerCaseCharactersText, + textViewIcon = binding.passwordPolicyLowerCaseCharactersIcon, + text = getString(R.string.password_policy_lowercase_characters, passwordPolicy.minLowercaseCharacters) + ) + } + } + + passwordPolicy.minSpecialCharacters?.let { minSpecialCharacters -> + if (minSpecialCharacters > 0) { + hasSpecialCharacter = password.count { SPECIALS_CHARACTERS.contains(it) } >= minSpecialCharacters + updateRequirement( + hasRequirement = hasSpecialCharacter, + layout = binding.passwordPolicyMinSpecialCharacters, + textView = binding.passwordPolicyMinSpecialCharactersText, + textViewIcon = binding.passwordPolicyMinSpecialCharactersIcon, + text = getString(R.string.password_policy_min_special_character, passwordPolicy.minSpecialCharacters, SPECIALS_CHARACTERS) + ) + } + } + + passwordPolicy.minDigits?.let { minDigits -> + if (minDigits > 0) { + hasDigit = password.count { it.isDigit() } >= minDigits + updateRequirement( + hasRequirement = hasDigit, + layout = binding.passwordPolicyMinDigits, + textView = binding.passwordPolicyMinDigitsText, + textViewIcon = binding.passwordPolicyMinDigitsIcon, + text = getString(R.string.password_policy_min_digits, passwordPolicy.minDigits) + ) + } + } + + val allConditionsCheck = hasMinCharacters && hasUpperCase && hasLowerCase && hasDigit && hasSpecialCharacter && hasMaxCharacters + enableButton(binding.setPasswordButton, allConditionsCheck) + enableButton(binding.copyPasswordButton, allConditionsCheck) + } + + private fun updateRequirement(hasRequirement: Boolean, layout: View, textView: TextView, textViewIcon: TextView, text: String) { + val textColor = if (hasRequirement) R.color.success else R.color.warning + val drawable = if (hasRequirement) R.drawable.ic_check_password_policy else R.drawable.ic_cross_warning_password_policy + + layout.isVisible = true + textView.apply { + setText(text) + setTextColor(ContextCompat.getColor(context, textColor)) + } + textViewIcon.setCompoundDrawablesRelativeWithIntrinsicBounds(drawable, 0, 0, 0) + } + + private fun enableButton(button: AppCompatButton, enable: Boolean) { + val textColor = if (enable) R.color.primary_button_background_color else R.color.grey + button.apply { + isEnabled = enable + setTextColor(ContextCompat.getColor(context, textColor)) + } + } + + interface SetPasswordListener { + fun onCancelPassword() + fun onSetPassword(password: String) + } + + companion object { + private const val ARG_ACCOUNT_NAME = "ARG_ACCOUNT_NAME" + private const val ARG_PASSWORD = "ARG_PASSWORD" + private const val SPECIALS_CHARACTERS = "!#$%&'()*+,-./:;<=>?@[\\]^_`{|}~" + + fun newInstance( + accountName: String, + password: String?, + listener: SetPasswordListener + ): SetPasswordDialogFragment { + val args = Bundle().apply { + putString(ARG_ACCOUNT_NAME, accountName) + putString(ARG_PASSWORD, password) + } + return SetPasswordDialogFragment().apply { + setPasswordListener = listener + arguments = args + } + } + } +} diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/links/SpaceLinksViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/links/SpaceLinksViewModel.kt new file mode 100644 index 00000000000..d5a1930ed2d --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/links/SpaceLinksViewModel.kt @@ -0,0 +1,55 @@ +/** + * ownCloud Android client application + * + * @author Jorge Aguado Recio + * + * Copyright (C) 2026 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.presentation.spaces.links + +import androidx.lifecycle.ViewModel +import com.owncloud.android.domain.links.model.OCLinkType +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update + +class SpaceLinksViewModel: ViewModel() { + + private val _addPublicLinkUIState = MutableStateFlow(null) + val addPublicLinkUIState: StateFlow = _addPublicLinkUIState + + init { + _addPublicLinkUIState.value = AddPublicLinkUIState() + } + + fun onPermissionSelected(permission: OCLinkType) { + _addPublicLinkUIState.update { it?.copy(selectedPermission = permission) } + } + + fun onExpirationDateSelected(expirationDate: String?) { + _addPublicLinkUIState.update { it?.copy(selectedExpirationDate = expirationDate) } + } + + fun onPasswordSelected(password: String?) { + _addPublicLinkUIState.update { it?.copy(selectedPassword = password) } + } + + data class AddPublicLinkUIState( + val selectedPermission: OCLinkType? = null, + val selectedExpirationDate: String? = null, + val selectedPassword: String? = null + ) +} diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersActivity.kt index 5ef7e74d270..51e4585d167 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersActivity.kt @@ -32,6 +32,7 @@ import com.owncloud.android.domain.roles.model.OCRole import com.owncloud.android.domain.spaces.model.OCSpace import com.owncloud.android.domain.spaces.model.SpaceMember import com.owncloud.android.presentation.common.ShareSheetHelper +import com.owncloud.android.presentation.spaces.links.AddPublicLinkFragment import com.owncloud.android.ui.activity.FileActivity import com.owncloud.android.utils.DisplayUtils @@ -100,6 +101,16 @@ class SpaceMembersActivity: FileActivity(), SpaceMembersFragment.SpaceMemberFrag } } + override fun addPublicLink() { + val addPublicLinkFragment = AddPublicLinkFragment.newInstance(account.name) + val transaction = supportFragmentManager.beginTransaction() + transaction.apply { + replace(R.id.members_fragment_container, addPublicLinkFragment, TAG_ADD_PUBLIC_LINK_FRAGMENT) + addToBackStack(null) + commit() + } + } + override fun copyOrSendPublicLink(publicLinkUrl: String, spaceName: String) { copyOrSendLink(publicLinkUrl, spaceName) } @@ -127,6 +138,7 @@ class SpaceMembersActivity: FileActivity(), SpaceMembersFragment.SpaceMemberFrag companion object { private const val TAG_SPACE_MEMBERS_FRAGMENT = "SPACE_MEMBERS_FRAGMENT" private const val TAG_ADD_MEMBER_FRAGMENT ="ADD_MEMBER_FRAGMENT" + private const val TAG_ADD_PUBLIC_LINK_FRAGMENT = "ADD_PUBLIC_LINK_FRAGMENT" private const val TYPE_PLAIN = "text/plain" private const val KEY_DISPLAY_NAME = "oc_display_name" private const val KEY_UUID = "oc_uuid" diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersFragment.kt index c96166ce202..227ab1943de 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersFragment.kt @@ -112,6 +112,10 @@ class SpaceMembersFragment : Fragment(), SpaceMembersAdapter.SpaceMembersAdapter selectedMember = null ) } + + binding.addPublicLinkButton.setOnClickListener { + listener?.addPublicLink() + } } override fun onActivityCreated(savedInstanceState: Bundle?) { @@ -300,6 +304,7 @@ class SpaceMembersFragment : Fragment(), SpaceMembersAdapter.SpaceMembersAdapter interface SpaceMemberFragmentListener { fun addMember(space: OCSpace, spaceMembers: List, roles: List, editMode: Boolean, selectedMember: SpaceMember?) + fun addPublicLink() fun copyOrSendPublicLink(publicLinkUrl: String, spaceName: String) } diff --git a/owncloudApp/src/main/res/drawable/ic_secret_file_drop.xml b/owncloudApp/src/main/res/drawable/ic_secret_file_drop.xml new file mode 100644 index 00000000000..f5a69e75311 --- /dev/null +++ b/owncloudApp/src/main/res/drawable/ic_secret_file_drop.xml @@ -0,0 +1,10 @@ + + + diff --git a/owncloudApp/src/main/res/layout/add_member_fragment.xml b/owncloudApp/src/main/res/layout/add_member_fragment.xml index 60f662286f5..b4326bcfca8 100644 --- a/owncloudApp/src/main/res/layout/add_member_fragment.xml +++ b/owncloudApp/src/main/res/layout/add_member_fragment.xml @@ -164,7 +164,7 @@ + layout="@layout/expiration_date_layout" /> diff --git a/owncloudApp/src/main/res/layout/add_public_link_fragment.xml b/owncloudApp/src/main/res/layout/add_public_link_fragment.xml new file mode 100644 index 00000000000..eed8912a50f --- /dev/null +++ b/owncloudApp/src/main/res/layout/add_public_link_fragment.xml @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/owncloudApp/src/main/res/layout/expiration_date_item.xml b/owncloudApp/src/main/res/layout/expiration_date_layout.xml similarity index 98% rename from owncloudApp/src/main/res/layout/expiration_date_item.xml rename to owncloudApp/src/main/res/layout/expiration_date_layout.xml index a6cbd2058ee..c17c486945d 100644 --- a/owncloudApp/src/main/res/layout/expiration_date_item.xml +++ b/owncloudApp/src/main/res/layout/expiration_date_layout.xml @@ -78,7 +78,7 @@ android:id="@+id/expiration_date_switch" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginEnd="@dimen/standard_quarter_margin" + android:layout_marginEnd="@dimen/standard_margin" android:contentDescription="@string/content_description_expiration_date_switch" app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/owncloudApp/src/main/res/layout/password_layout.xml b/owncloudApp/src/main/res/layout/password_layout.xml new file mode 100644 index 00000000000..fad5f541383 --- /dev/null +++ b/owncloudApp/src/main/res/layout/password_layout.xml @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/owncloudApp/src/main/res/layout/public_link_permissions.xml b/owncloudApp/src/main/res/layout/public_link_permissions.xml new file mode 100644 index 00000000000..7374c84daad --- /dev/null +++ b/owncloudApp/src/main/res/layout/public_link_permissions.xml @@ -0,0 +1,211 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/owncloudApp/src/main/res/layout/set_password_dialog.xml b/owncloudApp/src/main/res/layout/set_password_dialog.xml new file mode 100644 index 00000000000..807865c2fa9 --- /dev/null +++ b/owncloudApp/src/main/res/layout/set_password_dialog.xml @@ -0,0 +1,267 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/owncloudApp/src/main/res/values/strings.xml b/owncloudApp/src/main/res/values/strings.xml index 6ffef0c72ce..123c1e91991 100644 --- a/owncloudApp/src/main/res/values/strings.xml +++ b/owncloudApp/src/main/res/values/strings.xml @@ -218,6 +218,7 @@ unknown error Pending Important + Set Change password Remove account Create account @@ -829,6 +830,7 @@ %1$s space subtitle Create button Cancel button + Set Space quota User %1$s Group %1$s @@ -839,6 +841,11 @@ Remove member %1$s Edit member %1$s Get permanent link + Create public link + Set password + Remove password + Generate password + Copy password Create a shortcut URL @@ -905,10 +912,16 @@ User Group (me) + + Create public link + Set password Can view + View, download Can edit + View, upload, edit, download, delete Can upload Secret file drop + Upload only, existing content is not revealed Invited people forum or contribute in our GitHub repo]]> From 6e44d7fd424a3b8efe6fa14dac809a8c20576e9a Mon Sep 17 00:00:00 2001 From: joragua Date: Wed, 25 Mar 2026 12:18:23 +0100 Subject: [PATCH 3/5] refactor: remove redundant string in content descriptions for create and cancel buttons --- owncloudApp/src/main/res/values-de-rDE/strings.xml | 2 -- owncloudApp/src/main/res/values-de/strings.xml | 2 -- owncloudApp/src/main/res/values-en-rGB/strings.xml | 2 -- owncloudApp/src/main/res/values-en-rUS/strings.xml | 2 -- owncloudApp/src/main/res/values-es/strings.xml | 2 -- owncloudApp/src/main/res/values-et-rEE/strings.xml | 2 -- owncloudApp/src/main/res/values-ja-rJP/strings.xml | 2 -- owncloudApp/src/main/res/values-lo/strings.xml | 2 -- owncloudApp/src/main/res/values-pt-rBR/strings.xml | 2 -- owncloudApp/src/main/res/values-pt-rPT/strings.xml | 2 -- owncloudApp/src/main/res/values-sq/strings.xml | 2 -- owncloudApp/src/main/res/values/strings.xml | 4 ++-- 12 files changed, 2 insertions(+), 24 deletions(-) diff --git a/owncloudApp/src/main/res/values-de-rDE/strings.xml b/owncloudApp/src/main/res/values-de-rDE/strings.xml index 2119cd9e966..8f1c6d1f67b 100644 --- a/owncloudApp/src/main/res/values-de-rDE/strings.xml +++ b/owncloudApp/src/main/res/values-de-rDE/strings.xml @@ -824,8 +824,6 @@ %1$s Space Name %1$s Space Menü %1$s Space Untertitel - Erstellen-Schaltfläche - Abbruch-Schaltfläche Space Kontingent Benutzer %1$s Gruppe %1$s diff --git a/owncloudApp/src/main/res/values-de/strings.xml b/owncloudApp/src/main/res/values-de/strings.xml index 66363f5ec8f..f28122f4103 100644 --- a/owncloudApp/src/main/res/values-de/strings.xml +++ b/owncloudApp/src/main/res/values-de/strings.xml @@ -824,8 +824,6 @@ %1$s Space Name %1$s Space Menü %1$s Space Untertitel - Erstellen-Schaltfläche - Abbruch-Schaltfläche Space Kontingent Benutzer %1$s Gruppe %1$s diff --git a/owncloudApp/src/main/res/values-en-rGB/strings.xml b/owncloudApp/src/main/res/values-en-rGB/strings.xml index c603b2d70b6..94877c9c575 100644 --- a/owncloudApp/src/main/res/values-en-rGB/strings.xml +++ b/owncloudApp/src/main/res/values-en-rGB/strings.xml @@ -807,8 +807,6 @@ %1$s space name %1$s space menu %1$s space subtitle - Create button - Cancel button Space quota User %1$s Group %1$s diff --git a/owncloudApp/src/main/res/values-en-rUS/strings.xml b/owncloudApp/src/main/res/values-en-rUS/strings.xml index 60ef10ce182..92a1db28f3c 100644 --- a/owncloudApp/src/main/res/values-en-rUS/strings.xml +++ b/owncloudApp/src/main/res/values-en-rUS/strings.xml @@ -807,8 +807,6 @@ %1$s space name %1$s space menu %1$s space subtitle - Create button - Cancel button Space quota User %1$s Group %1$s diff --git a/owncloudApp/src/main/res/values-es/strings.xml b/owncloudApp/src/main/res/values-es/strings.xml index 2fbcabbf742..e3cc74e6fcc 100644 --- a/owncloudApp/src/main/res/values-es/strings.xml +++ b/owncloudApp/src/main/res/values-es/strings.xml @@ -825,8 +825,6 @@ %1$s nombre del space %1$s menu del space %1$s subtitulo del space - Botón Crear - Botón Cancelar Cuota del space Usuario %1$s Grupo %1$s diff --git a/owncloudApp/src/main/res/values-et-rEE/strings.xml b/owncloudApp/src/main/res/values-et-rEE/strings.xml index 196ccd1be5d..a7912856594 100644 --- a/owncloudApp/src/main/res/values-et-rEE/strings.xml +++ b/owncloudApp/src/main/res/values-et-rEE/strings.xml @@ -805,8 +805,6 @@ %1$s ruumi nimi %1$s ruumi menüü %1$s ruumi alapealkiri - Loomise nupp - Tühistamise nupp Ruumi mahupiirang Loo otsetee URL diff --git a/owncloudApp/src/main/res/values-ja-rJP/strings.xml b/owncloudApp/src/main/res/values-ja-rJP/strings.xml index 02008b9d24d..527813edf65 100644 --- a/owncloudApp/src/main/res/values-ja-rJP/strings.xml +++ b/owncloudApp/src/main/res/values-ja-rJP/strings.xml @@ -806,8 +806,6 @@ %1$sスペース名 %1$sスペースメニュー %1$sスペースサブタイトル - 作成ボタン - キャンセルボタン スペース割り当て ユーザー %1$s グループ %1$s diff --git a/owncloudApp/src/main/res/values-lo/strings.xml b/owncloudApp/src/main/res/values-lo/strings.xml index 4d88150ab5a..501a177ba48 100644 --- a/owncloudApp/src/main/res/values-lo/strings.xml +++ b/owncloudApp/src/main/res/values-lo/strings.xml @@ -801,8 +801,6 @@ ຊື່ພື້ນທີ່ ການດຳເນີນການກັບພື້ນທີ່ ລາຍລະອຽດພື້ນທີ່ - ສ້າງ - ຍົກເລີກ ຕັ້ງຄ່າໂຄຕ້າ ສ້າງທາງລັດ URL diff --git a/owncloudApp/src/main/res/values-pt-rBR/strings.xml b/owncloudApp/src/main/res/values-pt-rBR/strings.xml index 6752e657001..8c0bb7c8fd8 100644 --- a/owncloudApp/src/main/res/values-pt-rBR/strings.xml +++ b/owncloudApp/src/main/res/values-pt-rBR/strings.xml @@ -806,8 +806,6 @@ %1$s nome do espaço %1$s menu espacial %1$s legenda espacial - Botão Criar - Botão Cancelar Cota de espaço Crie um atalho URL diff --git a/owncloudApp/src/main/res/values-pt-rPT/strings.xml b/owncloudApp/src/main/res/values-pt-rPT/strings.xml index b9d2811e6c5..c3dadbd4148 100644 --- a/owncloudApp/src/main/res/values-pt-rPT/strings.xml +++ b/owncloudApp/src/main/res/values-pt-rPT/strings.xml @@ -806,8 +806,6 @@ %1$s nome do espaço %1$s menu espacial %1$s legenda espacial - Botão Criar - Botão Cancelar Cota de espaço Crie um atalho URL diff --git a/owncloudApp/src/main/res/values-sq/strings.xml b/owncloudApp/src/main/res/values-sq/strings.xml index 78efc58bdc3..bcc6462dd79 100644 --- a/owncloudApp/src/main/res/values-sq/strings.xml +++ b/owncloudApp/src/main/res/values-sq/strings.xml @@ -806,8 +806,6 @@ Emër hapësire %1$s Menu hapësire %1$s Nëntitull hapësire %1$s - Buton krijimi - Buton anulimi Kuota hapësire Përdorues %1$s Grup %1$s diff --git a/owncloudApp/src/main/res/values/strings.xml b/owncloudApp/src/main/res/values/strings.xml index 123c1e91991..8a922b77d81 100644 --- a/owncloudApp/src/main/res/values/strings.xml +++ b/owncloudApp/src/main/res/values/strings.xml @@ -828,8 +828,8 @@ %1$s space name %1$s space menu %1$s space subtitle - Create button - Cancel button + Create + Cancel Set Space quota User %1$s From d612636a13ce7544ed8868935db92d6a88395781 Mon Sep 17 00:00:00 2001 From: joragua Date: Wed, 25 Mar 2026 17:50:42 +0100 Subject: [PATCH 4/5] feat: implement methods and network operation to create a space link --- .../RemoteDataSourceModule.kt | 3 + .../dependecyinjection/RepositoryModule.kt | 3 + .../dependecyinjection/UseCaseModule.kt | 4 + .../spaces/links/AddPublicLinkFragment.kt | 24 ++++- .../spaces/links/SpaceLinksViewModel.kt | 32 ++++++- .../spaces/members/SpaceMembersActivity.kt | 4 +- .../spaces/members/SpaceMembersFragment.kt | 4 +- owncloudApp/src/main/res/values/strings.xml | 1 + .../resources/links/AddRemoteLinkOperation.kt | 88 +++++++++++++++++++ .../resources/links/services/LinksService.kt | 28 ++++++ .../links/services/OCLinksService.kt | 43 +++++++++ .../owncloud/android/data/ClientManager.kt | 7 ++ .../datasources/RemoteLinksDataSource.kt | 25 ++++++ .../implementation/OCRemoteLinksDataSource.kt | 34 +++++++ .../links/repository/OCLinksRepository.kt | 33 +++++++ .../android/domain/links/LinksRepository.kt | 25 ++++++ .../android/domain/links/model/OCLink.kt | 23 ++++- .../domain/links/usecases/AddLinkUseCase.kt | 42 +++++++++ 18 files changed, 411 insertions(+), 12 deletions(-) create mode 100644 owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/links/AddRemoteLinkOperation.kt create mode 100644 owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/links/services/LinksService.kt create mode 100644 owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/links/services/OCLinksService.kt create mode 100644 owncloudData/src/main/java/com/owncloud/android/data/links/datasources/RemoteLinksDataSource.kt create mode 100644 owncloudData/src/main/java/com/owncloud/android/data/links/datasources/implementation/OCRemoteLinksDataSource.kt create mode 100644 owncloudData/src/main/java/com/owncloud/android/data/links/repository/OCLinksRepository.kt create mode 100644 owncloudDomain/src/main/java/com/owncloud/android/domain/links/LinksRepository.kt create mode 100644 owncloudDomain/src/main/java/com/owncloud/android/domain/links/usecases/AddLinkUseCase.kt diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RemoteDataSourceModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RemoteDataSourceModule.kt index 14efaaac634..483b90e6e11 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RemoteDataSourceModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RemoteDataSourceModule.kt @@ -33,6 +33,8 @@ import com.owncloud.android.data.capabilities.datasources.implementation.OCRemot import com.owncloud.android.data.capabilities.datasources.mapper.RemoteCapabilityMapper import com.owncloud.android.data.files.datasources.RemoteFileDataSource import com.owncloud.android.data.files.datasources.implementation.OCRemoteFileDataSource +import com.owncloud.android.data.links.datasources.RemoteLinksDataSource +import com.owncloud.android.data.links.datasources.implementation.OCRemoteLinksDataSource import com.owncloud.android.data.members.datasources.RemoteMembersDataSource import com.owncloud.android.data.members.datasources.implementation.OCRemoteMembersDataSource import com.owncloud.android.data.oauth.datasources.RemoteOAuthDataSource @@ -78,6 +80,7 @@ val remoteDataSourceModule = module { singleOf(::OCRemoteAuthenticationDataSource) bind RemoteAuthenticationDataSource::class singleOf(::OCRemoteCapabilitiesDataSource) bind RemoteCapabilitiesDataSource::class singleOf(::OCRemoteFileDataSource) bind RemoteFileDataSource::class + singleOf(::OCRemoteLinksDataSource) bind RemoteLinksDataSource::class singleOf(::OCRemoteMembersDataSource) bind RemoteMembersDataSource::class singleOf(::OCRemoteOAuthDataSource) bind RemoteOAuthDataSource::class singleOf(::OCRemoteRolesDataSource) bind RemoteRolesDataSource::class diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RepositoryModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RepositoryModule.kt index 44e1e762781..3135cde74e6 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RepositoryModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RepositoryModule.kt @@ -28,6 +28,7 @@ import com.owncloud.android.data.authentication.repository.OCAuthenticationRepos import com.owncloud.android.data.capabilities.repository.OCCapabilityRepository import com.owncloud.android.data.files.repository.OCFileRepository import com.owncloud.android.data.folderbackup.repository.OCFolderBackupRepository +import com.owncloud.android.data.links.repository.OCLinksRepository import com.owncloud.android.data.members.repository.OCMembersRepository import com.owncloud.android.data.oauth.repository.OCOAuthRepository import com.owncloud.android.data.roles.repository.OCRolesRepository @@ -44,6 +45,7 @@ import com.owncloud.android.domain.authentication.oauth.OAuthRepository import com.owncloud.android.domain.automaticuploads.FolderBackupRepository import com.owncloud.android.domain.capabilities.CapabilityRepository import com.owncloud.android.domain.files.FileRepository +import com.owncloud.android.domain.links.LinksRepository import com.owncloud.android.domain.members.MembersRepository import com.owncloud.android.domain.roles.RolesRepository import com.owncloud.android.domain.server.ServerInfoRepository @@ -63,6 +65,7 @@ val repositoryModule = module { factoryOf(::OCCapabilityRepository) bind CapabilityRepository::class factoryOf(::OCFileRepository) bind FileRepository::class factoryOf(::OCFolderBackupRepository) bind FolderBackupRepository::class + factoryOf(::OCLinksRepository) bind LinksRepository::class factoryOf(::OCMembersRepository) bind MembersRepository::class factoryOf(::OCOAuthRepository) bind OAuthRepository::class factoryOf(::OCRolesRepository) bind RolesRepository::class diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt index b53e5bd983a..a771da5d52c 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt @@ -79,6 +79,7 @@ import com.owncloud.android.domain.files.usecases.SetLastUsageFileUseCase import com.owncloud.android.domain.files.usecases.SortFilesUseCase import com.owncloud.android.domain.files.usecases.SortFilesWithSyncInfoUseCase import com.owncloud.android.domain.files.usecases.UpdateAlreadyDownloadedFilesPathUseCase +import com.owncloud.android.domain.links.usecases.AddLinkUseCase import com.owncloud.android.domain.members.usecases.AddMemberUseCase import com.owncloud.android.domain.members.usecases.EditMemberUseCase import com.owncloud.android.domain.members.usecases.RemoveMemberUseCase @@ -316,4 +317,7 @@ val useCaseModule = module { factoryOf(::EditMemberUseCase) factoryOf(::RemoveMemberUseCase) factoryOf(::SearchMembersUseCase) + + // Links + factoryOf(::AddLinkUseCase) } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/links/AddPublicLinkFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/links/AddPublicLinkFragment.kt index afa7e6afe73..4ccf56976bb 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/links/AddPublicLinkFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/links/AddPublicLinkFragment.kt @@ -34,6 +34,7 @@ import com.owncloud.android.databinding.AddPublicLinkFragmentBinding import com.owncloud.android.domain.capabilities.model.CapabilityBooleanType import com.owncloud.android.domain.capabilities.model.OCCapability import com.owncloud.android.domain.links.model.OCLinkType +import com.owncloud.android.domain.spaces.model.OCSpace import com.owncloud.android.extensions.collectLatestLifecycleFlow import com.owncloud.android.presentation.capabilities.CapabilityViewModel import com.owncloud.android.presentation.common.UIResult @@ -49,7 +50,12 @@ class AddPublicLinkFragment: Fragment(), SetPasswordDialogFragment.SetPasswordLi private var _binding: AddPublicLinkFragmentBinding? = null private val binding get() = _binding!! - private val spaceLinksViewModel: SpaceLinksViewModel by viewModel() + private val spaceLinksViewModel: SpaceLinksViewModel by viewModel { + parametersOf( + requireArguments().getString(ARG_ACCOUNT_NAME), + requireArguments().getParcelable(ARG_CURRENT_SPACE) + ) + } private val capabilityViewModel: CapabilityViewModel by viewModel { parametersOf( requireArguments().getString(ARG_ACCOUNT_NAME) @@ -85,7 +91,7 @@ class AddPublicLinkFragment: Fragment(), SetPasswordDialogFragment.SetPasswordLi } hasPassword = it.selectedPassword != null - it.selectedPermission?.let { + it.selectedPermission?.let { selectedPermission -> binding.optionsLayout.isVisible = true binding.passwordLayout.apply { passwordValue.isVisible = hasPassword @@ -95,6 +101,15 @@ class AddPublicLinkFragment: Fragment(), SetPasswordDialogFragment.SetPasswordLi setPasswordSwitch.isChecked = hasPassword } binding.createPublicLinkButton.isEnabled = isPasswordEnforced && hasPassword || !isPasswordEnforced + + binding.createPublicLinkButton.setOnClickListener { + spaceLinksViewModel.createPublicLink( + binding.publicLinkNameEditText.text.toString().ifEmpty { getString(R.string.public_link_default_display_name) }, + selectedPermission, + uiState.selectedExpirationDate, + uiState.selectedPassword, + ) + } } bindDatePickerDialog(uiState.selectedExpirationDate) @@ -253,12 +268,15 @@ class AddPublicLinkFragment: Fragment(), SetPasswordDialogFragment.SetPasswordLi companion object { private const val DIALOG_SET_PASSWORD = "DIALOG_SET_PASSWORD" private const val ARG_ACCOUNT_NAME = "ARG_ACCOUNT_NAME" + private const val ARG_CURRENT_SPACE = "ARG_CURRENT_SPACE" fun newInstance( - accountName: String + accountName: String, + currentSpace: OCSpace ): AddPublicLinkFragment { val args = Bundle().apply { putString(ARG_ACCOUNT_NAME, accountName) + putParcelable(ARG_CURRENT_SPACE, currentSpace) } return AddPublicLinkFragment().apply { arguments = args diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/links/SpaceLinksViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/links/SpaceLinksViewModel.kt index d5a1930ed2d..96216255feb 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/links/SpaceLinksViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/links/SpaceLinksViewModel.kt @@ -22,15 +22,29 @@ package com.owncloud.android.presentation.spaces.links import androidx.lifecycle.ViewModel import com.owncloud.android.domain.links.model.OCLinkType +import com.owncloud.android.domain.links.usecases.AddLinkUseCase +import com.owncloud.android.domain.spaces.model.OCSpace +import com.owncloud.android.domain.utils.Event +import com.owncloud.android.extensions.ViewModelExt.runUseCaseWithResult +import com.owncloud.android.presentation.common.UIResult +import com.owncloud.android.providers.CoroutinesDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update -class SpaceLinksViewModel: ViewModel() { +class SpaceLinksViewModel( + private val addLinkUseCase: AddLinkUseCase, + private val accountName: String, + private val space: OCSpace, + private val coroutineDispatcherProvider: CoroutinesDispatcherProvider +): ViewModel() { private val _addPublicLinkUIState = MutableStateFlow(null) val addPublicLinkUIState: StateFlow = _addPublicLinkUIState + private val _addLinkResultFlow = MutableStateFlow>?>(null) + val addLinkResultFlow: StateFlow>?> = _addLinkResultFlow + init { _addPublicLinkUIState.value = AddPublicLinkUIState() } @@ -47,6 +61,22 @@ class SpaceLinksViewModel: ViewModel() { _addPublicLinkUIState.update { it?.copy(selectedPassword = password) } } + fun createPublicLink(displayName: String, permission: OCLinkType, expirationDate: String?, password: String?) { + runUseCaseWithResult( + coroutineDispatcher = coroutineDispatcherProvider.io, + flow = _addLinkResultFlow, + useCase = addLinkUseCase, + useCaseParams = AddLinkUseCase.Params( + accountName = accountName, + spaceId = space.id, + displayName = displayName, + type = OCLinkType.toString(permission), + expirationDate = expirationDate, + password = password + ) + ) + } + data class AddPublicLinkUIState( val selectedPermission: OCLinkType? = null, val selectedExpirationDate: String? = null, diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersActivity.kt index 51e4585d167..fb2fe90acbc 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersActivity.kt @@ -101,8 +101,8 @@ class SpaceMembersActivity: FileActivity(), SpaceMembersFragment.SpaceMemberFrag } } - override fun addPublicLink() { - val addPublicLinkFragment = AddPublicLinkFragment.newInstance(account.name) + override fun addPublicLink(space: OCSpace) { + val addPublicLinkFragment = AddPublicLinkFragment.newInstance(account.name, space) val transaction = supportFragmentManager.beginTransaction() transaction.apply { replace(R.id.members_fragment_container, addPublicLinkFragment, TAG_ADD_PUBLIC_LINK_FRAGMENT) diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersFragment.kt index 227ab1943de..0bababe465b 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersFragment.kt @@ -114,7 +114,7 @@ class SpaceMembersFragment : Fragment(), SpaceMembersAdapter.SpaceMembersAdapter } binding.addPublicLinkButton.setOnClickListener { - listener?.addPublicLink() + listener?.addPublicLink(currentSpace) } } @@ -304,7 +304,7 @@ class SpaceMembersFragment : Fragment(), SpaceMembersAdapter.SpaceMembersAdapter interface SpaceMemberFragmentListener { fun addMember(space: OCSpace, spaceMembers: List, roles: List, editMode: Boolean, selectedMember: SpaceMember?) - fun addPublicLink() + fun addPublicLink(space: OCSpace) fun copyOrSendPublicLink(publicLinkUrl: String, spaceName: String) } diff --git a/owncloudApp/src/main/res/values/strings.xml b/owncloudApp/src/main/res/values/strings.xml index 8a922b77d81..26af175cec9 100644 --- a/owncloudApp/src/main/res/values/strings.xml +++ b/owncloudApp/src/main/res/values/strings.xml @@ -923,6 +923,7 @@ Secret file drop Upload only, existing content is not revealed Invited people + Unnamed Link forum or contribute in our GitHub repo]]> diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/links/AddRemoteLinkOperation.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/links/AddRemoteLinkOperation.kt new file mode 100644 index 00000000000..4357466c526 --- /dev/null +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/links/AddRemoteLinkOperation.kt @@ -0,0 +1,88 @@ +/** + * ownCloud Android client application + * + * @author Jorge Aguado Recio + * + * Copyright (C) 2026 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.lib.resources.links + +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.http.HttpConstants +import com.owncloud.android.lib.common.http.HttpConstants.CONTENT_TYPE_JSON +import com.owncloud.android.lib.common.http.methods.nonwebdav.PostMethod +import com.owncloud.android.lib.common.operations.RemoteOperation +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody +import org.json.JSONObject +import timber.log.Timber +import java.net.URL + +class AddRemoteLinkOperation( + private val spaceId: String, + private val displayName: String, + private val type: String, + private val expirationDate: String?, + private val password: String? +): RemoteOperation() { + override fun run(client: OwnCloudClient): RemoteOperationResult { + var result: RemoteOperationResult + try { + val uriBuilder = client.baseUri.buildUpon().apply { + appendEncodedPath(GRAPH_API_DRIVES_PATH) + appendEncodedPath(spaceId) + appendEncodedPath(GRAPH_API_CREATE_LINK_PATH) + } + + val requestBody = JSONObject().apply { + put(DISPLAY_NAME_BODY_PARAM, displayName) + expirationDate?.let { put(EXPIRATION_DATE_BODY_PARAM, it) } + password?.let { put(PASSWORD_BODY_PARAM, it) } + put(TYPE_BODY_PARAM, type) + }.toString().toRequestBody(CONTENT_TYPE_JSON.toMediaType()) + + val postMethod = PostMethod(URL(uriBuilder.build().toString()), requestBody) + + val status = client.executeHttpMethod(postMethod) + + val response = postMethod.getResponseBodyAsString() + + if (status == HttpConstants.HTTP_OK) { + Timber.d("Successful response: $response") + result = RemoteOperationResult(ResultCode.OK) + Timber.d("Add a public link operation completed and parsed to ${result.data}") + } else { + result = RemoteOperationResult(postMethod) + Timber.e("Failed response while adding a public link; status code: $status, response: $response") + } + } catch (e: Exception) { + result = RemoteOperationResult(e) + Timber.e(e, "Exception while adding a public link") + } + return result + } + + companion object { + private const val GRAPH_API_DRIVES_PATH = "graph/v1beta1/drives/" + private const val GRAPH_API_CREATE_LINK_PATH = "root/createLink" + private const val DISPLAY_NAME_BODY_PARAM = "displayName" + private const val EXPIRATION_DATE_BODY_PARAM = "expirationDateTime" + private const val PASSWORD_BODY_PARAM = "password" + private const val TYPE_BODY_PARAM = "type" + } +} diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/links/services/LinksService.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/links/services/LinksService.kt new file mode 100644 index 00000000000..f88face3482 --- /dev/null +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/links/services/LinksService.kt @@ -0,0 +1,28 @@ +/** + * ownCloud Android client application + * + * @author Jorge Aguado Recio + * + * Copyright (C) 2026 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.lib.resources.links.services + +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.resources.Service + +interface LinksService: Service { + fun addLink(spaceId: String, displayName: String, type: String, expirationDate: String?, password: String?): RemoteOperationResult +} diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/links/services/OCLinksService.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/links/services/OCLinksService.kt new file mode 100644 index 00000000000..15fdf5207a7 --- /dev/null +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/links/services/OCLinksService.kt @@ -0,0 +1,43 @@ +/** + * ownCloud Android client application + * + * @author Jorge Aguado Recio + * + * Copyright (C) 2026 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.lib.resources.links.services + +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.resources.links.AddRemoteLinkOperation + +class OCLinksService(override val client: OwnCloudClient) : LinksService { + + override fun addLink( + spaceId: String, + displayName: String, + type: String, + expirationDate: String?, + password: String? + ): RemoteOperationResult = + AddRemoteLinkOperation( + spaceId = spaceId, + displayName = displayName, + type = type, + expirationDate = expirationDate, + password = password + ).execute(client) +} diff --git a/owncloudData/src/main/java/com/owncloud/android/data/ClientManager.kt b/owncloudData/src/main/java/com/owncloud/android/data/ClientManager.kt index b4681c050c8..e0fa7997e96 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/ClientManager.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/ClientManager.kt @@ -36,6 +36,8 @@ import com.owncloud.android.lib.resources.appregistry.services.AppRegistryServic import com.owncloud.android.lib.resources.appregistry.services.OCAppRegistryService import com.owncloud.android.lib.resources.files.services.FileService import com.owncloud.android.lib.resources.files.services.implementation.OCFileService +import com.owncloud.android.lib.resources.links.services.LinksService +import com.owncloud.android.lib.resources.links.services.OCLinksService import com.owncloud.android.lib.resources.members.services.MembersService import com.owncloud.android.lib.resources.members.services.OCMembersService import com.owncloud.android.lib.resources.roles.services.OCRolesService @@ -176,4 +178,9 @@ class ClientManager( val ownCloudClient = getClientForAccount(accountName) return OCMembersService(client = ownCloudClient) } + + fun getLinksService(accountName: String): LinksService { + val ownCloudClient = getClientForAccount(accountName) + return OCLinksService(client = ownCloudClient) + } } diff --git a/owncloudData/src/main/java/com/owncloud/android/data/links/datasources/RemoteLinksDataSource.kt b/owncloudData/src/main/java/com/owncloud/android/data/links/datasources/RemoteLinksDataSource.kt new file mode 100644 index 00000000000..d3e8ef88577 --- /dev/null +++ b/owncloudData/src/main/java/com/owncloud/android/data/links/datasources/RemoteLinksDataSource.kt @@ -0,0 +1,25 @@ +/** + * ownCloud Android client application + * + * @author Jorge Aguado Recio + * + * Copyright (C) 2026 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.data.links.datasources + +interface RemoteLinksDataSource { + fun addLink(accountName: String, spaceId: String, displayName: String, type: String, expirationDate: String?, password: String?) +} diff --git a/owncloudData/src/main/java/com/owncloud/android/data/links/datasources/implementation/OCRemoteLinksDataSource.kt b/owncloudData/src/main/java/com/owncloud/android/data/links/datasources/implementation/OCRemoteLinksDataSource.kt new file mode 100644 index 00000000000..f038a93e883 --- /dev/null +++ b/owncloudData/src/main/java/com/owncloud/android/data/links/datasources/implementation/OCRemoteLinksDataSource.kt @@ -0,0 +1,34 @@ +/** + * ownCloud Android client application + * + * @author Jorge Aguado Recio + * + * Copyright (C) 2026 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.data.links.datasources.implementation + +import com.owncloud.android.data.ClientManager +import com.owncloud.android.data.executeRemoteOperation +import com.owncloud.android.data.links.datasources.RemoteLinksDataSource + +class OCRemoteLinksDataSource( + private val clientManager: ClientManager +): RemoteLinksDataSource { + + override fun addLink(accountName: String, spaceId: String, displayName: String, type: String, expirationDate: String?, password: String?) { + executeRemoteOperation { clientManager.getLinksService(accountName).addLink(spaceId, displayName, type, expirationDate, password) } + } +} diff --git a/owncloudData/src/main/java/com/owncloud/android/data/links/repository/OCLinksRepository.kt b/owncloudData/src/main/java/com/owncloud/android/data/links/repository/OCLinksRepository.kt new file mode 100644 index 00000000000..0cea80e5dae --- /dev/null +++ b/owncloudData/src/main/java/com/owncloud/android/data/links/repository/OCLinksRepository.kt @@ -0,0 +1,33 @@ +/** + * ownCloud Android client application + * + * @author Jorge Aguado Recio + * + * Copyright (C) 2026 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.data.links.repository + +import com.owncloud.android.data.links.datasources.RemoteLinksDataSource +import com.owncloud.android.domain.links.LinksRepository + +class OCLinksRepository( + private val remoteLinksDataSource: RemoteLinksDataSource +): LinksRepository { + + override fun addLink(accountName: String, spaceId: String, displayName: String, type: String, expirationDate: String?, password: String?) { + remoteLinksDataSource.addLink(accountName, spaceId, displayName, type, expirationDate, password) + } +} diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/links/LinksRepository.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/links/LinksRepository.kt new file mode 100644 index 00000000000..8ff4a8cca9f --- /dev/null +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/links/LinksRepository.kt @@ -0,0 +1,25 @@ +/** + * ownCloud Android client application + * + * @author Jorge Aguado Recio + * + * Copyright (C) 2026 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.domain.links + +interface LinksRepository { + fun addLink(accountName: String, spaceId: String, displayName: String, type: String, expirationDate: String?, password: String?) +} diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/links/model/OCLink.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/links/model/OCLink.kt index a44e1061565..7cfa5d7b881 100644 --- a/owncloudDomain/src/main/java/com/owncloud/android/domain/links/model/OCLink.kt +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/links/model/OCLink.kt @@ -38,13 +38,28 @@ enum class OCLinkType { CAN_VIEW, CAN_EDIT, CAN_UPLOAD, CREATE_ONLY, INTERNAL; companion object { + private const val CAN_VIEW_TYPE_STRING = "view" + private const val CAN_EDIT_TYPE_STRING = "edit" + private const val CAN_UPLOAD_TYPE_STRING = "upload" + private const val CREATE_ONLY_TYPE_STRING = "createOnly" + private const val INTERNAL_TYPE_STRING = "internal" + fun parseFromString(type: String): OCLinkType = when (type) { - "view" -> CAN_VIEW - "edit" -> CAN_EDIT - "upload" -> CAN_UPLOAD - "createOnly" -> CREATE_ONLY + CAN_VIEW_TYPE_STRING -> CAN_VIEW + CAN_EDIT_TYPE_STRING -> CAN_EDIT + CAN_UPLOAD_TYPE_STRING -> CAN_UPLOAD + CREATE_ONLY_TYPE_STRING -> CREATE_ONLY else -> INTERNAL } + + fun toString(type: OCLinkType): String = + when (type) { + CAN_VIEW -> CAN_VIEW_TYPE_STRING + CAN_EDIT -> CAN_EDIT_TYPE_STRING + CAN_UPLOAD -> CAN_UPLOAD_TYPE_STRING + CREATE_ONLY -> CREATE_ONLY_TYPE_STRING + INTERNAL -> INTERNAL_TYPE_STRING + } } } diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/links/usecases/AddLinkUseCase.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/links/usecases/AddLinkUseCase.kt new file mode 100644 index 00000000000..08a552c407d --- /dev/null +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/links/usecases/AddLinkUseCase.kt @@ -0,0 +1,42 @@ +/** + * ownCloud Android client application + * + * @author Jorge Aguado Recio + * + * Copyright (C) 2026 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.domain.links.usecases + +import com.owncloud.android.domain.BaseUseCaseWithResult +import com.owncloud.android.domain.links.LinksRepository + +class AddLinkUseCase( + private val linksRepository: LinksRepository +): BaseUseCaseWithResult() { + + override fun run(params: Params) { + linksRepository.addLink(params.accountName, params.spaceId, params.displayName, params.type, params.expirationDate, params.password) + } + + data class Params( + val accountName: String, + val spaceId: String, + val displayName: String, + val type: String, + val expirationDate: String?, + val password: String? + ) +} From e3a92305d00d5058ea2a73a4e46579033eb2798f Mon Sep 17 00:00:00 2001 From: joragua Date: Thu, 26 Mar 2026 08:48:05 +0100 Subject: [PATCH 5/5] feat: show snackbar after executing add public link operation --- .../spaces/links/AddPublicLinkFragment.kt | 14 ++++++- .../spaces/members/SpaceMembersFragment.kt | 39 +++++++++++++++++++ owncloudApp/src/main/res/values/strings.xml | 2 + 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/links/AddPublicLinkFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/links/AddPublicLinkFragment.kt index 4ccf56976bb..0c7b2a1f5c2 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/links/AddPublicLinkFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/links/AddPublicLinkFragment.kt @@ -36,9 +36,11 @@ import com.owncloud.android.domain.capabilities.model.OCCapability import com.owncloud.android.domain.links.model.OCLinkType import com.owncloud.android.domain.spaces.model.OCSpace import com.owncloud.android.extensions.collectLatestLifecycleFlow +import com.owncloud.android.extensions.showErrorInSnackbar import com.owncloud.android.presentation.capabilities.CapabilityViewModel import com.owncloud.android.presentation.common.UIResult import com.owncloud.android.utils.DisplayUtils +import org.koin.androidx.viewmodel.ext.android.activityViewModel import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf import timber.log.Timber @@ -50,7 +52,7 @@ class AddPublicLinkFragment: Fragment(), SetPasswordDialogFragment.SetPasswordLi private var _binding: AddPublicLinkFragmentBinding? = null private val binding get() = _binding!! - private val spaceLinksViewModel: SpaceLinksViewModel by viewModel { + private val spaceLinksViewModel: SpaceLinksViewModel by activityViewModel { parametersOf( requireArguments().getString(ARG_ACCOUNT_NAME), requireArguments().getParcelable(ARG_CURRENT_SPACE) @@ -147,6 +149,16 @@ class AddPublicLinkFragment: Fragment(), SetPasswordDialogFragment.SetPasswordLi } } + collectLatestLifecycleFlow(spaceLinksViewModel.addLinkResultFlow) { event -> + event?.peekContent()?.let { uiResult -> + when (uiResult) { + is UIResult.Loading -> { } + is UIResult.Success -> parentFragmentManager.popBackStack() + is UIResult.Error -> showErrorInSnackbar(R.string.public_link_add_failed, uiResult.error) + } + } + } + binding.publicLinkPermissions.apply { canViewPublicLinkRadioButton.setOnClickListener { selectRadioButton(canViewPublicLinkRadioButton) } canViewPublicLinkLayout.setOnClickListener { selectRadioButton(canViewPublicLinkRadioButton) } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersFragment.kt index 0bababe465b..d907dd17110 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/members/SpaceMembersFragment.kt @@ -42,6 +42,7 @@ import com.owncloud.android.extensions.showErrorInSnackbar import com.owncloud.android.extensions.showMessageInSnackbar import com.owncloud.android.presentation.common.UIResult import com.owncloud.android.presentation.spaces.links.SpaceLinksAdapter +import com.owncloud.android.presentation.spaces.links.SpaceLinksViewModel import com.owncloud.android.utils.DisplayUtils import org.koin.androidx.viewmodel.ext.android.activityViewModel import org.koin.core.parameter.parametersOf @@ -61,6 +62,12 @@ class SpaceMembersFragment : Fragment(), SpaceMembersAdapter.SpaceMembersAdapter requireArguments().getParcelable(ARG_CURRENT_SPACE) ) } + private val spaceLinksViewModel: SpaceLinksViewModel by activityViewModel { + parametersOf( + requireArguments().getString(ARG_ACCOUNT_NAME), + requireArguments().getParcelable(ARG_CURRENT_SPACE) + ) + } private lateinit var spaceMembersAdapter: SpaceMembersAdapter private lateinit var spaceLinksAdapter: SpaceLinksAdapter @@ -170,6 +177,16 @@ class SpaceMembersFragment : Fragment(), SpaceMembersAdapter.SpaceMembersAdapter } private fun subscribeToViewModels() { + observeRoles() + observeSpaceMembers() + observeSpacePermissions() + observeAddMemberResult() + observeRemoveMemberResult() + observeEditMemberResult() + observeAddLinkResult() + } + + private fun observeRoles() { collectLatestLifecycleFlow(spaceMembersViewModel.roles) { event -> event?.let { when (val uiResult = event.peekContent()) { @@ -186,7 +203,9 @@ class SpaceMembersFragment : Fragment(), SpaceMembersAdapter.SpaceMembersAdapter } } } + } + private fun observeSpaceMembers() { collectLatestLifecycleFlow(spaceMembersViewModel.spaceMembers) { event -> event?.let { when (val uiResult = event.peekContent()) { @@ -213,7 +232,9 @@ class SpaceMembersFragment : Fragment(), SpaceMembersAdapter.SpaceMembersAdapter } } } + } + private fun observeSpacePermissions() { collectLatestLifecycleFlow(spaceMembersViewModel.spacePermissions) { event -> event?.let { when (val uiResult = event.peekContent()) { @@ -230,7 +251,9 @@ class SpaceMembersFragment : Fragment(), SpaceMembersAdapter.SpaceMembersAdapter } } } + } + private fun observeAddMemberResult() { collectLatestLifecycleFlow(spaceMembersViewModel.addMemberResultFlow) { event -> event?.peekContent()?.let { uiResult -> when (uiResult) { @@ -243,7 +266,9 @@ class SpaceMembersFragment : Fragment(), SpaceMembersAdapter.SpaceMembersAdapter } } } + } + private fun observeRemoveMemberResult() { collectLatestLifecycleFlow(spaceMembersViewModel.removeMemberResultFlow) { uiResult -> when (uiResult) { is UIResult.Loading -> { } @@ -257,7 +282,9 @@ class SpaceMembersFragment : Fragment(), SpaceMembersAdapter.SpaceMembersAdapter } } } + } + private fun observeEditMemberResult() { collectLatestLifecycleFlow(spaceMembersViewModel.editMemberResultFlow) { event -> event?.peekContent()?.let { uiResult -> when (uiResult) { @@ -272,6 +299,18 @@ class SpaceMembersFragment : Fragment(), SpaceMembersAdapter.SpaceMembersAdapter } } + private fun observeAddLinkResult() { + collectLatestLifecycleFlow(spaceLinksViewModel.addLinkResultFlow) { event -> + event?.peekContent()?.let { uiResult -> + when (uiResult) { + is UIResult.Loading -> { } + is UIResult.Success -> showMessageInSnackbar(getString(R.string.public_link_add_correctly)) + is UIResult.Error -> { } + } + } + } + } + private fun checkPermissions(spacePermissions: List) { binding.apply { val hasCreatePermission = DRIVES_CREATE_PERMISSION in spacePermissions diff --git a/owncloudApp/src/main/res/values/strings.xml b/owncloudApp/src/main/res/values/strings.xml index 26af175cec9..d95fa82bc53 100644 --- a/owncloudApp/src/main/res/values/strings.xml +++ b/owncloudApp/src/main/res/values/strings.xml @@ -924,6 +924,8 @@ Upload only, existing content is not revealed Invited people Unnamed Link + Public link created correctly + Public link could not be created forum or contribute in our GitHub repo]]>