Skip to content

Commit d6efcfa

Browse files
committed
feat: add layout for creating a public link over a space
1 parent 52c9589 commit d6efcfa

14 files changed

Lines changed: 1400 additions & 3 deletions

owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* @author David Crespo Ríos
88
* @author Jorge Aguado Recio
99
*
10-
* Copyright (C) 2025 ownCloud GmbH.
10+
* Copyright (C) 2026 ownCloud GmbH.
1111
*
1212
* This program is free software: you can redistribute it and/or modify
1313
* it under the terms of the GNU General Public License version 2,
@@ -55,6 +55,7 @@ import com.owncloud.android.presentation.settings.more.SettingsMoreViewModel
5555
import com.owncloud.android.presentation.settings.security.SettingsSecurityViewModel
5656
import com.owncloud.android.presentation.sharing.ShareViewModel
5757
import com.owncloud.android.presentation.spaces.SpacesListViewModel
58+
import com.owncloud.android.presentation.spaces.links.SpaceLinksViewModel
5859
import com.owncloud.android.presentation.spaces.members.SpaceMembersViewModel
5960
import com.owncloud.android.presentation.transfers.TransfersViewModel
6061
import com.owncloud.android.ui.ReceiveExternalFilesViewModel
@@ -85,6 +86,7 @@ val viewModelModule = module {
8586
viewModelOf(::SettingsSecurityViewModel)
8687
viewModelOf(::SettingsVideoUploadsViewModel)
8788
viewModelOf(::SettingsViewModel)
89+
viewModelOf(::SpaceLinksViewModel)
8890
viewModelOf(::SpaceMembersViewModel)
8991
viewModelOf(::FileOperationsViewModel)
9092

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
/**
2+
* ownCloud Android client application
3+
*
4+
* @author Jorge Aguado Recio
5+
*
6+
* Copyright (C) 2026 ownCloud GmbH.
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU General Public License version 2,
10+
* as published by the Free Software Foundation.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU General Public License
18+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
19+
*/
20+
21+
package com.owncloud.android.presentation.spaces.links
22+
23+
import android.app.DatePickerDialog
24+
import android.icu.util.Calendar
25+
import android.os.Bundle
26+
import android.view.LayoutInflater
27+
import android.view.View
28+
import android.view.ViewGroup
29+
import android.widget.RadioButton
30+
import androidx.core.view.isVisible
31+
import androidx.fragment.app.Fragment
32+
import com.owncloud.android.R
33+
import com.owncloud.android.databinding.CreatePublicLinkFragmentBinding
34+
import com.owncloud.android.domain.capabilities.model.CapabilityBooleanType
35+
import com.owncloud.android.domain.capabilities.model.OCCapability
36+
import com.owncloud.android.domain.links.model.OCLinkType
37+
import com.owncloud.android.extensions.collectLatestLifecycleFlow
38+
import com.owncloud.android.presentation.capabilities.CapabilityViewModel
39+
import com.owncloud.android.presentation.common.UIResult
40+
import com.owncloud.android.utils.DisplayUtils
41+
import org.koin.androidx.viewmodel.ext.android.viewModel
42+
import org.koin.core.parameter.parametersOf
43+
import timber.log.Timber
44+
import java.text.SimpleDateFormat
45+
import java.util.Locale
46+
import java.util.TimeZone
47+
48+
class CreateSpacePublicLinkFragment: Fragment(), SetPasswordDialogFragment.SetPasswordListener {
49+
private var _binding: CreatePublicLinkFragmentBinding? = null
50+
private val binding get() = _binding!!
51+
52+
private val spaceLinksViewModel: SpaceLinksViewModel by viewModel()
53+
private val capabilityViewModel: CapabilityViewModel by viewModel {
54+
parametersOf(
55+
requireArguments().getString(ARG_ACCOUNT_NAME)
56+
)
57+
}
58+
59+
private var capabilities: OCCapability? = null
60+
private var isPasswordEnforced = true
61+
private var hasPassword = false
62+
63+
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
64+
_binding = CreatePublicLinkFragmentBinding.inflate(inflater, container, false)
65+
return binding.root
66+
}
67+
68+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
69+
super.onViewCreated(view, savedInstanceState)
70+
requireActivity().setTitle(R.string.public_link_create_title)
71+
72+
binding.publicLinkPermissions.apply {
73+
canViewPublicLinkRadioButton.tag = OCLinkType.CAN_VIEW
74+
canEditPublicLinkRadioButton.tag = OCLinkType.CAN_EDIT
75+
secretFileDropPublicLinkRadioButton.tag = OCLinkType.CREATE_ONLY
76+
}
77+
78+
collectLatestLifecycleFlow(spaceLinksViewModel.addPublicLinkUIState) { uiState ->
79+
uiState?.let {
80+
it.selectedExpirationDate?.let { expirationDate ->
81+
binding.expirationDateLayout.expirationDateValue.apply {
82+
visibility = View.VISIBLE
83+
text = DisplayUtils.displayDateToHumanReadable(expirationDate)
84+
}
85+
}
86+
87+
hasPassword = it.selectedPassword != null
88+
it.selectedPermission?.let {
89+
binding.optionsLayout.isVisible = true
90+
binding.passwordLayout.apply {
91+
passwordValue.isVisible = hasPassword
92+
setPasswordButton.isVisible = !hasPassword && isPasswordEnforced
93+
removePasswordButton.isVisible = hasPassword && isPasswordEnforced
94+
setPasswordSwitch.isVisible = !isPasswordEnforced
95+
setPasswordSwitch.isChecked = hasPassword
96+
}
97+
binding.createPublicLinkButton.isEnabled = isPasswordEnforced && hasPassword || !isPasswordEnforced
98+
}
99+
100+
bindDatePickerDialog(uiState.selectedExpirationDate)
101+
102+
binding.expirationDateLayout.apply {
103+
expirationDateLayout.setOnClickListener {
104+
if (uiState.selectedExpirationDate != null) {
105+
openDatePickerDialog(uiState.selectedExpirationDate)
106+
} else {
107+
expirationDateSwitch.isChecked = true
108+
}
109+
}
110+
}
111+
112+
binding.passwordLayout.apply {
113+
passwordLayout.setOnClickListener {
114+
if (!isPasswordEnforced){
115+
setPasswordSwitch.isChecked = true
116+
}
117+
showPasswordDialog(uiState.selectedPassword)
118+
}
119+
}
120+
}
121+
}
122+
123+
capabilityViewModel.capabilities.observe(viewLifecycleOwner) { event->
124+
when (val uiResult = event.peekContent()) {
125+
is UIResult.Success -> {
126+
capabilities = uiResult.data
127+
}
128+
is UIResult.Loading -> { }
129+
is UIResult.Error -> {
130+
Timber.e(uiResult.error, "Failed to retrieve server capabilities")
131+
}
132+
}
133+
}
134+
135+
binding.publicLinkPermissions.apply {
136+
canViewPublicLinkRadioButton.setOnClickListener { selectRadioButton(canViewPublicLinkRadioButton) }
137+
canViewPublicLinkLayout.setOnClickListener { selectRadioButton(canViewPublicLinkRadioButton) }
138+
canEditPublicLinkRadioButton.setOnClickListener { selectRadioButton(canEditPublicLinkRadioButton) }
139+
canEditPublicLinkLayout.setOnClickListener { selectRadioButton(canEditPublicLinkRadioButton) }
140+
secretFileDropPublicLinkRadioButton.setOnClickListener { selectRadioButton(secretFileDropPublicLinkRadioButton) }
141+
secretFileDropPublicLinkLayout.setOnClickListener { selectRadioButton(secretFileDropPublicLinkRadioButton) }
142+
}
143+
144+
binding.passwordLayout.apply {
145+
setPasswordButton.setOnClickListener {
146+
showPasswordDialog()
147+
}
148+
removePasswordButton.setOnClickListener {
149+
removePassword()
150+
}
151+
setPasswordSwitch.setOnClickListener {
152+
if (setPasswordSwitch.isChecked) showPasswordDialog() else removePassword()
153+
}
154+
}
155+
}
156+
157+
override fun onCancelPassword() {
158+
if (!isPasswordEnforced && !hasPassword) {
159+
binding.passwordLayout.setPasswordSwitch.isChecked = false
160+
}
161+
}
162+
163+
override fun onSetPassword(password: String) {
164+
spaceLinksViewModel.onPasswordSelected(password)
165+
}
166+
167+
private fun selectRadioButton(selectedRadioButton: RadioButton) {
168+
binding.publicLinkPermissions.apply {
169+
canViewPublicLinkRadioButton.isChecked = false
170+
canEditPublicLinkRadioButton.isChecked = false
171+
secretFileDropPublicLinkRadioButton.isChecked = false
172+
selectedRadioButton.isChecked = true
173+
}
174+
val selectedPermission = selectedRadioButton.tag as OCLinkType
175+
checkPasswordEnforced(selectedPermission)
176+
spaceLinksViewModel.onPermissionSelected(selectedPermission)
177+
}
178+
179+
private fun checkPasswordEnforced(selectedPermission: OCLinkType) {
180+
isPasswordEnforced = when (selectedPermission) {
181+
OCLinkType.CAN_VIEW -> {
182+
capabilities?.filesSharingPublicPasswordEnforcedReadOnly == CapabilityBooleanType.TRUE
183+
}
184+
OCLinkType.CAN_EDIT -> {
185+
capabilities?.filesSharingPublicPasswordEnforcedReadWrite == CapabilityBooleanType.TRUE
186+
}
187+
OCLinkType.CREATE_ONLY -> {
188+
capabilities?.filesSharingPublicPasswordEnforcedUploadOnly == CapabilityBooleanType.TRUE
189+
}
190+
else -> {
191+
true
192+
}
193+
}
194+
}
195+
196+
private fun bindDatePickerDialog(expirationDate: String?) {
197+
binding.expirationDateLayout.expirationDateSwitch.setOnCheckedChangeListener { _, isChecked ->
198+
if (isChecked) {
199+
openDatePickerDialog(expirationDate)
200+
} else {
201+
binding.expirationDateLayout.expirationDateValue.visibility = View.GONE
202+
spaceLinksViewModel.onExpirationDateSelected(null)
203+
}
204+
}
205+
}
206+
207+
private fun openDatePickerDialog(expirationDate: String?) {
208+
val calendar = Calendar.getInstance()
209+
val formatter = SimpleDateFormat(DisplayUtils.DATE_FORMAT_ISO, Locale.ROOT).apply {
210+
timeZone = TimeZone.getTimeZone("UTC")
211+
}
212+
213+
expirationDate?.let {
214+
calendar.time = formatter.parse(it)
215+
}
216+
217+
DatePickerDialog(
218+
requireContext(),
219+
{ _, selectedYear, selectedMonth, selectedDay ->
220+
calendar.set(selectedYear, selectedMonth, selectedDay, 23, 59, 59)
221+
calendar.set(Calendar.MILLISECOND, 999)
222+
val isoExpirationDate = formatter.format(calendar.time)
223+
spaceLinksViewModel.onExpirationDateSelected(isoExpirationDate)
224+
binding.expirationDateLayout.expirationDateValue.apply {
225+
visibility = View.VISIBLE
226+
text = DisplayUtils.displayDateToHumanReadable(isoExpirationDate)
227+
}
228+
},
229+
calendar.get(Calendar.YEAR),
230+
calendar.get(Calendar.MONTH),
231+
calendar.get(Calendar.DAY_OF_MONTH)
232+
).apply {
233+
datePicker.minDate = Calendar.getInstance().timeInMillis
234+
show()
235+
setOnCancelListener {
236+
if (expirationDate == null) {
237+
binding.expirationDateLayout.expirationDateSwitch.isChecked = false
238+
}
239+
}
240+
}
241+
}
242+
243+
private fun showPasswordDialog(password: String? = null) {
244+
val accountName = requireArguments().getString(ARG_ACCOUNT_NAME) ?: return
245+
val dialog = SetPasswordDialogFragment.newInstance(accountName, password, this)
246+
dialog.show(parentFragmentManager, DIALOG_SET_PASSWORD)
247+
}
248+
249+
private fun removePassword() {
250+
spaceLinksViewModel.onPasswordSelected(null)
251+
}
252+
253+
companion object {
254+
private const val DIALOG_SET_PASSWORD = "DIALOG_SET_PASSWORD"
255+
private const val ARG_ACCOUNT_NAME = "ARG_ACCOUNT_NAME"
256+
257+
fun newInstance(
258+
accountName: String
259+
): CreateSpacePublicLinkFragment {
260+
val args = Bundle().apply {
261+
putString(ARG_ACCOUNT_NAME, accountName)
262+
}
263+
return CreateSpacePublicLinkFragment().apply {
264+
arguments = args
265+
}
266+
}
267+
}
268+
}

0 commit comments

Comments
 (0)