diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/LinkShareViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/LinkShareViewHolder.kt index 3ebe6b08d380..5409a793d298 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/LinkShareViewHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/LinkShareViewHolder.kt @@ -98,26 +98,33 @@ internal class LinkShareViewHolder(itemView: View) : RecyclerView.ViewHolder(ite } } - private fun setSubline(binding: FileDetailsShareLinkShareItemBinding?, context: Context?, publicShare: OCShare) { + /** + * Displays download limit information when available, hides subline otherwise. + * Clears text on hide to prevent RecyclerView item reuse issues. + */ + private fun setSubline( + binding: FileDetailsShareLinkShareItemBinding?, + context: Context?, + publicShare: OCShare + ) { if (binding == null || context == null) { return } - val downloadLimit = publicShare.fileDownloadLimit - if (downloadLimit != null) { - val remaining = publicShare.remainingDownloadLimit() ?: return - val text = context.resources.getQuantityString( + val remaining = publicShare.remainingDownloadLimit()?.coerceAtLeast(0) + + if (remaining == null) { + // No download limit set at all + binding.subline.text = "" + binding.subline.visibility = View.GONE + } else { + val binding.subline.text = context.resources.getQuantityString( R.plurals.share_download_limit_description, remaining, remaining ) - - binding.subline.text = text binding.subline.visibility = View.VISIBLE - return } - - binding.subline.visibility = View.GONE } private fun setPermissionName( diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailsSharingProcessFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailsSharingProcessFragment.kt index d97dfc911c40..be099b4e35df 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailsSharingProcessFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailsSharingProcessFragment.kt @@ -42,6 +42,8 @@ import com.owncloud.android.utils.ClipboardUtil import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.theme.CapabilityUtils import com.owncloud.android.utils.theme.ViewThemeUtils +import java.text.NumberFormat +import java.text.ParseException import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -491,22 +493,24 @@ class FileDetailsSharingProcessFragment : } } + /** + * Pre-fills download limit controls with current remaining count for existing share. + * Clamps negative values to 0. Returns early if no limit is set. + */ private fun updateFileDownloadLimitView() { if (!canSetDownloadLimit()) { return } - // user can set download limit thus no need to rely on current limit to show download limit - showFileDownloadLimitInput(true) + showFileDownloadLimitInput(true) // only other option binding.shareProcessSetDownloadLimitSwitch.visibility = View.VISIBLE binding.shareProcessSetDownloadLimitInput.visibility = View.VISIBLE - val currentLimit = share?.remainingDownloadLimit() ?: return - if (currentLimit > 0) { - binding.shareProcessSetDownloadLimitSwitch.isChecked = true - binding.shareProcessSetDownloadLimitInput.setText( - "%d".format(Locale.getDefault(), currentLimit) - ) + val currentLimit = share?.remainingDownloadLimit()?.coerceAtLeast(0) ?: return + + binding.shareProcessSetDownloadLimitSwitch.isChecked = true + binding.shareProcessSetDownloadLimitInput.setText( + NumberFormat.getInstance(Locale.getDefault()).format(currentLimit) } } @@ -844,16 +848,50 @@ class FileDetailsSharingProcessFragment : } } + /** + * Validates and applies the download limit from the input field. + * + * Parses the user's input using locale-aware number formatting to handle + * grouping separators (e.g., "1,000" in US or "1.000" in Germany). + * Shows inline error messages for invalid input. + * + * @see [updateFileDownloadLimitView] for how the field is populated + */ private fun setDownloadLimit() { + if (!binding.shareProcessSetDownloadLimitSwitch.isChecked) { + // User wants to remove the download limit + fileOperationsHelper?.updateFilesDownloadLimit(share, 0) + return + } + val downloadLimitInput = binding.shareProcessSetDownloadLimitInput.text.toString().trim() - val downloadLimit = - if (binding.shareProcessSetDownloadLimitSwitch.isChecked && downloadLimitInput.isNotEmpty()) { - downloadLimitInput.toInt() - } else { - 0 - } - fileOperationsHelper?.updateFilesDownloadLimit(share, downloadLimit) + if (downloadLimitInput.isEmpty()) { + binding.shareProcessSetDownloadLimitInput.error = getString(R.string.share_download_limit_required) + return + } + + // Parse with locale awareness to match formatting in updateFileDownloadLimitView + val downloadLimit = try { + NumberFormat.getInstance(Locale.getDefault()) + .parse(downloadLimitInput)?.toInt() + } catch (e: ParseException) { + null + } + + when { + downloadLimit == null -> { + binding.shareProcessSetDownloadLimitInput.error = getString(R.string.share_download_limit_invalid) + return + } + downloadLimit <= 0 -> { + binding.shareProcessSetDownloadLimitInput.error = getString(R.string.share_download_limit_must_be_positive) + return + } + else -> { + fileOperationsHelper?.updateFilesDownloadLimit(share, downloadLimit) + } + } } private fun createShare(noteText: String) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b4cac075161e..656da2b0de7e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1452,9 +1452,13 @@ Unable to set download limit. Please check capabilities. Download limit + No downloads remaining %1$d download remaining %1$d downloads remaining + Download limit is required + Please enter a valid number + Download limit must be greater than 0 Please manually check terms of service! Terms of service I agree to the above ToS