From a39add5a5162f55fb56f87d3f75ba71cfcda306e Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 8 Jul 2026 14:32:06 +0200 Subject: [PATCH 1/5] use uri Signed-off-by: alperozturk96 --- .../android/files/FetchTemplateOperation.java | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/files/FetchTemplateOperation.java b/app/src/main/java/com/owncloud/android/files/FetchTemplateOperation.java index e16fafa6f562..4eddbc660e20 100644 --- a/app/src/main/java/com/owncloud/android/files/FetchTemplateOperation.java +++ b/app/src/main/java/com/owncloud/android/files/FetchTemplateOperation.java @@ -7,6 +7,8 @@ */ package com.owncloud.android.files; +import android.net.Uri; + import com.owncloud.android.datamodel.Template; import com.owncloud.android.lib.common.OwnCloudClient; import com.owncloud.android.lib.common.operations.RemoteOperation; @@ -26,14 +28,15 @@ public class FetchTemplateOperation extends RemoteOperation { private static final String TAG = FetchTemplateOperation.class.getSimpleName(); private static final int SYNC_READ_TIMEOUT = 40000; private static final int SYNC_CONNECTION_TIMEOUT = 5000; - private static final String TEMPLATE_URL = "/ocs/v2.php/apps/richdocuments/api/v1/templates/"; + private static final String TEMPLATE_ROUTE = "ocs/v2.php/apps/richdocuments/api/v1/templates"; + private static final String PARAM_FORMAT = "format"; + private static final String VALUE_FORMAT_JSON = "json"; private ChooseRichDocumentsTemplateDialogFragment.Type type; // JSON node names private static final String NODE_OCS = "ocs"; private static final String NODE_DATA = "data"; - private static final String JSON_FORMAT = "?format=json"; public FetchTemplateOperation(ChooseRichDocumentsTemplateDialogFragment.Type type) { this.type = type; @@ -44,9 +47,14 @@ protected RemoteOperationResult run(OwnCloudClient client) { GetMethod getMethod = null; try { + Uri uri = client.getBaseUri() + .buildUpon() + .appendEncodedPath(TEMPLATE_ROUTE) + .appendEncodedPath(type.toString().toLowerCase(Locale.ENGLISH)) + .appendQueryParameter(PARAM_FORMAT, VALUE_FORMAT_JSON) + .build(); - getMethod = new GetMethod(client.getBaseUri() + TEMPLATE_URL + type.toString().toLowerCase(Locale.ENGLISH) + - JSON_FORMAT); + getMethod = new GetMethod(uri.toString()); // remote request getMethod.addRequestHeader(OCS_API_HEADER, OCS_API_HEADER_VALUE); @@ -76,8 +84,9 @@ protected RemoteOperationResult run(OwnCloudClient client) { result = new RemoteOperationResult(true, getMethod); result.setData(templateArray); } else { + String errorBody = getMethod.getResponseBodyAsString(); result = new RemoteOperationResult(false, getMethod); - client.exhaustResponse(getMethod.getResponseBodyAsStream()); + Log_OC.e(TAG, "Get templates for type " + type + " failed with status " + status + ": " + errorBody); } } catch (Exception e) { result = new RemoteOperationResult(e); From 50cd3af42748ce55c25d3d3d1e3af2fb650d5d18 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 8 Jul 2026 14:32:20 +0200 Subject: [PATCH 2/5] wip Signed-off-by: alperozturk96 --- .../ui/dialog/ChooseRichDocumentsTemplateDialogFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/ChooseRichDocumentsTemplateDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/ChooseRichDocumentsTemplateDialogFragment.kt index 1afec56af969..32b104be8319 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/ChooseRichDocumentsTemplateDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/ChooseRichDocumentsTemplateDialogFragment.kt @@ -301,7 +301,7 @@ class ChooseRichDocumentsTemplateDialogFragment : name, fileDataStorageManager.getCapability(currentAccount.user), requireContext(), - fileNames ?: setOf() + fileNames ) return when { From 4dffc4ab5fdafb307241d2c9594c5818a516f03e Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 7 Sep 2026 11:49:16 +0200 Subject: [PATCH 3/5] revert(templates): drop unrelated dialog change The redundant elvis on fileNames belongs to neither the crash fix nor the Kotlin conversion, so it goes back to its previous form to keep this branch scoped to one concern. Assisted-by: ClaudeCode:claude-opus-5 --- .../ui/dialog/ChooseRichDocumentsTemplateDialogFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/ChooseRichDocumentsTemplateDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/ChooseRichDocumentsTemplateDialogFragment.kt index 32b104be8319..1afec56af969 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/ChooseRichDocumentsTemplateDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/ChooseRichDocumentsTemplateDialogFragment.kt @@ -301,7 +301,7 @@ class ChooseRichDocumentsTemplateDialogFragment : name, fileDataStorageManager.getCapability(currentAccount.user), requireContext(), - fileNames + fileNames ?: setOf() ) return when { From 80d18da6011edee49fe70d53c59b251d6b6b40e9 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 7 Sep 2026 11:49:25 +0200 Subject: [PATCH 4/5] fix(templates): keep create dialog usable when no template loads When the template list comes back empty the dialog had no selected template, so the extension check compared the typed name against null and reported "Extension cannot be changed" for every name while Create stayed disabled. Resolve the filename into a single state, treat "no template selected" as its own state, hold the error back until the user has typed, and close the dialog when no template could be loaded. Assisted-by: ClaudeCode:claude-opus-5 --- .../ui/dialog/ChooseTemplateDialogFragment.kt | 95 +++++++++---------- .../ui/dialog/TemplateFilenameState.kt | 16 ++++ 2 files changed, 59 insertions(+), 52 deletions(-) create mode 100644 app/src/main/java/com/owncloud/android/ui/dialog/TemplateFilenameState.kt diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt index 733205a9db3a..44c8bab6819b 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt @@ -65,6 +65,7 @@ class ChooseTemplateDialogFragment : Injectable { private lateinit var fileNames: MutableSet + private var hasUserInteracted = false @Inject lateinit var clientFactory: ClientFactory @@ -142,6 +143,7 @@ class ChooseTemplateDialogFragment : override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) = Unit override fun afterTextChanged(s: Editable) { + hasUserInteracted = true checkFileNameAfterEachType() } }) @@ -224,71 +226,59 @@ class ChooseTemplateDialogFragment : private fun getOCCapability(): OCCapability = fileDataStorageManager.getCapability(currentAccount.user.accountName) override fun onClick(v: View) { + val selectedTemplate = adapter?.selectedTemplate + ?: return DisplayUtils.showSnackMessage(binding.list, R.string.select_one_template) + + val state = resolveFilenameState() + if (state !is TemplateFilenameState.Valid) { + state.errorMessage?.let { DisplayUtils.showSnackMessage(requireActivity(), it.toString()) } + return + } + val name = binding.filename.text.toString() val path = parentFolder?.remotePath + name - val selectedTemplate = adapter?.selectedTemplate + val fullPath = if (name.endsWith(selectedTemplate.extension)) { + path + } else { + path + DOT + selectedTemplate.extension + } - val errorMessage = FileNameValidator.checkFileName(name, getOCCapability(), requireContext()) + createFromTemplate(selectedTemplate, fullPath) + } - when { - selectedTemplate == null -> { - DisplayUtils.showSnackMessage(binding.list, R.string.select_one_template) - } + private fun resolveFilenameState(): TemplateFilenameState { + val selectedTemplate = adapter?.selectedTemplate ?: return TemplateFilenameState.NoTemplateSelected + val name = binding.filename.text.toString().trim() + val validationError = FileNameValidator.checkFileName(name, getOCCapability(), requireContext(), fileNames) - errorMessage != null -> { - DisplayUtils.showSnackMessage(requireActivity(), errorMessage) - } + return when { + name.equals(DOT + selectedTemplate.extension, ignoreCase = true) -> + TemplateFilenameState.JustExtension(getString(R.string.enter_filename)) - name.equals(DOT + selectedTemplate.extension, ignoreCase = true) -> { - DisplayUtils.showSnackMessage(binding.list, R.string.enter_filename) - } + validationError != null -> TemplateFilenameState.Invalid(validationError) - else -> { - val fullPath = if (!name.endsWith(selectedTemplate.extension)) { - path + DOT + selectedTemplate.extension - } else { - path - } - createFromTemplate(selectedTemplate, fullPath) - } + FileNameValidator.isFileHidden(name) -> + TemplateFilenameState.HiddenName(getText(R.string.hidden_file_name_warning)) + + name.substringAfterLast(DOT) != selectedTemplate.extension -> + TemplateFilenameState.ChangedExtension(getString(R.string.extension_cannot_be_changed)) + + else -> TemplateFilenameState.Valid } } private fun checkFileNameAfterEachType() { - if (positiveButton == null) return + val positiveButton = positiveButton ?: return + val state = resolveFilenameState() - val selectedTemplate = adapter?.selectedTemplate - val name = binding.filename.text.toString().trim() - val isNameJustExtension = selectedTemplate != null && - name.equals( - DOT + selectedTemplate.extension, - ignoreCase = true - ) - val fileNameValidatorResult = - FileNameValidator.checkFileName(name, getOCCapability(), requireContext(), fileNames) - - val errorMessage = when { - isNameJustExtension -> null - fileNameValidatorResult != null -> fileNameValidatorResult - else -> null - } + val isValid = state is TemplateFilenameState.Valid + positiveButton.isEnabled = isValid + positiveButton.isClickable = isValid - val isNameValid = (errorMessage == null) && !name.equals(DOT + selectedTemplate?.extension, ignoreCase = true) - val isHiddenFileName = FileNameValidator.isFileHidden(name) - val isChangedExtension = name.substringAfterLast(DOT) != selectedTemplate?.extension + if (!hasUserInteracted) return - binding.filenameContainer.isErrorEnabled = !isNameValid || isHiddenFileName || isChangedExtension - binding.filenameContainer.error = when { - !isNameValid -> errorMessage ?: getString(R.string.enter_filename) - isHiddenFileName -> getText(R.string.hidden_file_name_warning) - isChangedExtension -> getString(R.string.extension_cannot_be_changed) - else -> null - } - - positiveButton?.apply { - isEnabled = isNameValid && !isHiddenFileName && !isChangedExtension - isClickable = isEnabled - } + binding.filenameContainer.isErrorEnabled = state.errorMessage != null + binding.filenameContainer.error = state.errorMessage } @Suppress("LongParameterList", "DEPRECATION") @@ -406,7 +396,8 @@ class ChooseTemplateDialogFragment : } if (templateList.templates.isEmpty()) { - DisplayUtils.showSnackMessage(fragment.binding.list, R.string.error_retrieving_templates) + fragment.dismiss() + DisplayUtils.showSnackMessage(fragment.requireActivity(), R.string.error_retrieving_templates) return } diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/TemplateFilenameState.kt b/app/src/main/java/com/owncloud/android/ui/dialog/TemplateFilenameState.kt new file mode 100644 index 000000000000..f300d4c1b9ea --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/dialog/TemplateFilenameState.kt @@ -0,0 +1,16 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.dialog + +sealed class TemplateFilenameState(val errorMessage: CharSequence?) { + data object Valid : TemplateFilenameState(null) + data object NoTemplateSelected : TemplateFilenameState(null) + class JustExtension(message: CharSequence) : TemplateFilenameState(message) + class HiddenName(message: CharSequence) : TemplateFilenameState(message) + class ChangedExtension(message: CharSequence) : TemplateFilenameState(message) + class Invalid(message: CharSequence) : TemplateFilenameState(message) +} From b3c6bf8d641a230c06363a65740e7351fc7f4d20 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 7 Sep 2026 11:49:25 +0200 Subject: [PATCH 5/5] refactor(templates): convert FetchTemplateOperation to Kotlin Fail-fast control flow, JSON node names as constants, and URL building and response parsing split into separate functions. Behaviour is unchanged. Assisted-by: ClaudeCode:claude-opus-5 --- .../android/files/FetchTemplateOperation.java | 102 ------------------ .../android/files/FetchTemplateOperation.kt | 81 ++++++++++++++ 2 files changed, 81 insertions(+), 102 deletions(-) delete mode 100644 app/src/main/java/com/owncloud/android/files/FetchTemplateOperation.java create mode 100644 app/src/main/java/com/owncloud/android/files/FetchTemplateOperation.kt diff --git a/app/src/main/java/com/owncloud/android/files/FetchTemplateOperation.java b/app/src/main/java/com/owncloud/android/files/FetchTemplateOperation.java deleted file mode 100644 index 4eddbc660e20..000000000000 --- a/app/src/main/java/com/owncloud/android/files/FetchTemplateOperation.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2018 Tobias Kaminsky - * SPDX-FileCopyrightText: 2018 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.owncloud.android.files; - -import android.net.Uri; - -import com.owncloud.android.datamodel.Template; -import com.owncloud.android.lib.common.OwnCloudClient; -import com.owncloud.android.lib.common.operations.RemoteOperation; -import com.owncloud.android.lib.common.operations.RemoteOperationResult; -import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.ui.dialog.ChooseRichDocumentsTemplateDialogFragment; - -import org.apache.commons.httpclient.HttpStatus; -import org.apache.commons.httpclient.methods.GetMethod; -import org.json.JSONArray; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.Locale; - -public class FetchTemplateOperation extends RemoteOperation { - private static final String TAG = FetchTemplateOperation.class.getSimpleName(); - private static final int SYNC_READ_TIMEOUT = 40000; - private static final int SYNC_CONNECTION_TIMEOUT = 5000; - private static final String TEMPLATE_ROUTE = "ocs/v2.php/apps/richdocuments/api/v1/templates"; - private static final String PARAM_FORMAT = "format"; - private static final String VALUE_FORMAT_JSON = "json"; - - private ChooseRichDocumentsTemplateDialogFragment.Type type; - - // JSON node names - private static final String NODE_OCS = "ocs"; - private static final String NODE_DATA = "data"; - - public FetchTemplateOperation(ChooseRichDocumentsTemplateDialogFragment.Type type) { - this.type = type; - } - - protected RemoteOperationResult run(OwnCloudClient client) { - RemoteOperationResult result; - GetMethod getMethod = null; - - try { - Uri uri = client.getBaseUri() - .buildUpon() - .appendEncodedPath(TEMPLATE_ROUTE) - .appendEncodedPath(type.toString().toLowerCase(Locale.ENGLISH)) - .appendQueryParameter(PARAM_FORMAT, VALUE_FORMAT_JSON) - .build(); - - getMethod = new GetMethod(uri.toString()); - - // remote request - getMethod.addRequestHeader(OCS_API_HEADER, OCS_API_HEADER_VALUE); - - int status = client.executeMethod(getMethod, SYNC_READ_TIMEOUT, SYNC_CONNECTION_TIMEOUT); - - if (status == HttpStatus.SC_OK) { - String response = getMethod.getResponseBodyAsString(); - - // Parse the response - JSONObject respJSON = new JSONObject(response); - JSONArray templates = respJSON.getJSONObject(NODE_OCS).getJSONArray(NODE_DATA); - - ArrayList templateArray = new ArrayList<>(); - - for (int i = 0; i < templates.length(); i++) { - JSONObject templateObject = templates.getJSONObject(i); - - templateArray.add(new Template(templateObject.getLong("id"), - templateObject.getString("name"), - templateObject.optString("preview"), - Template.Type.parse(templateObject.getString("type") - .toUpperCase(Locale.ROOT)), - templateObject.getString("extension"))); - } - - result = new RemoteOperationResult(true, getMethod); - result.setData(templateArray); - } else { - String errorBody = getMethod.getResponseBodyAsString(); - result = new RemoteOperationResult(false, getMethod); - Log_OC.e(TAG, "Get templates for type " + type + " failed with status " + status + ": " + errorBody); - } - } catch (Exception e) { - result = new RemoteOperationResult(e); - Log_OC.e(TAG, "Get templates for typ " + type + " failed: " + result.getLogMessage(), - result.getException()); - } finally { - if (getMethod != null) { - getMethod.releaseConnection(); - } - } - return result; - } -} diff --git a/app/src/main/java/com/owncloud/android/files/FetchTemplateOperation.kt b/app/src/main/java/com/owncloud/android/files/FetchTemplateOperation.kt new file mode 100644 index 000000000000..7c37b4d0576a --- /dev/null +++ b/app/src/main/java/com/owncloud/android/files/FetchTemplateOperation.kt @@ -0,0 +1,81 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.files + +import com.owncloud.android.datamodel.Template +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.operations.RemoteOperation +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.ui.dialog.ChooseRichDocumentsTemplateDialogFragment +import org.apache.commons.httpclient.HttpStatus +import org.apache.commons.httpclient.methods.GetMethod +import org.json.JSONObject + +class FetchTemplateOperation(private val type: ChooseRichDocumentsTemplateDialogFragment.Type) : + RemoteOperation() { + + @Suppress("TooGenericExceptionCaught") + override fun run(client: OwnCloudClient): RemoteOperationResult { + var getMethod: GetMethod? = null + + return try { + getMethod = GetMethod(templateUrl(client.baseUri.toString())).apply { + addRequestHeader(OCS_API_HEADER, OCS_API_HEADER_VALUE) + } + + val status = client.executeMethod(getMethod, SYNC_READ_TIMEOUT, SYNC_CONNECTION_TIMEOUT) + if (status != HttpStatus.SC_OK) { + client.exhaustResponse(getMethod.responseBodyAsStream) + return RemoteOperationResult(false, getMethod) + } + + val templates = parseTemplates(getMethod.responseBodyAsString) + RemoteOperationResult(true, getMethod).apply { setData(ArrayList(templates)) } + } catch (e: Exception) { + RemoteOperationResult(e).also { + Log_OC.e(TAG, "Get templates for type $type failed: ${it.logMessage}", it.exception) + } + } finally { + getMethod?.releaseConnection() + } + } + + private fun templateUrl(baseUri: String): String = baseUri + TEMPLATE_URL + type.name.lowercase() + JSON_FORMAT + + private fun parseTemplates(response: String): List