From 3627a9174701e1839406f2f7628929fd36834b3d 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 52804d3b090fb66397c866c4f938343534082dc9 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 5bc650b5efc46f46e48035e49db722f83fef443f 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 9d0d88cce8cd71381b4c587f39c65a45ccf93cc0 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 0442a1ef3c33649e521aaed7314a761b232039d8 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