Skip to content

Commit 3115d0b

Browse files
Merge pull request #17569 from nextcloud/refactor/fetch-template
improve: fetch-template
2 parents 185885b + f290682 commit 3115d0b

4 files changed

Lines changed: 140 additions & 145 deletions

File tree

app/src/main/java/com/owncloud/android/files/FetchTemplateOperation.java

Lines changed: 0 additions & 93 deletions
This file was deleted.
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
package com.owncloud.android.files
8+
9+
import com.owncloud.android.datamodel.Template
10+
import com.owncloud.android.lib.common.OwnCloudClient
11+
import com.owncloud.android.lib.common.operations.RemoteOperation
12+
import com.owncloud.android.lib.common.operations.RemoteOperationResult
13+
import com.owncloud.android.lib.common.utils.Log_OC
14+
import com.owncloud.android.ui.dialog.ChooseRichDocumentsTemplateDialogFragment
15+
import org.apache.commons.httpclient.HttpStatus
16+
import org.apache.commons.httpclient.methods.GetMethod
17+
import org.json.JSONObject
18+
19+
class FetchTemplateOperation(private val type: ChooseRichDocumentsTemplateDialogFragment.Type) :
20+
RemoteOperation<Any>() {
21+
22+
@Suppress("TooGenericExceptionCaught")
23+
override fun run(client: OwnCloudClient): RemoteOperationResult<Any> {
24+
var getMethod: GetMethod? = null
25+
26+
return try {
27+
getMethod = GetMethod(templateUrl(client.baseUri.toString())).apply {
28+
addRequestHeader(OCS_API_HEADER, OCS_API_HEADER_VALUE)
29+
}
30+
31+
val status = client.executeMethod(getMethod, SYNC_READ_TIMEOUT, SYNC_CONNECTION_TIMEOUT)
32+
if (status != HttpStatus.SC_OK) {
33+
client.exhaustResponse(getMethod.responseBodyAsStream)
34+
return RemoteOperationResult(false, getMethod)
35+
}
36+
37+
val templates = parseTemplates(getMethod.responseBodyAsString)
38+
RemoteOperationResult<Any>(true, getMethod).apply { setData(ArrayList<Any>(templates)) }
39+
} catch (e: Exception) {
40+
RemoteOperationResult<Any>(e).also {
41+
Log_OC.e(TAG, "Get templates for type $type failed: ${it.logMessage}", it.exception)
42+
}
43+
} finally {
44+
getMethod?.releaseConnection()
45+
}
46+
}
47+
48+
private fun templateUrl(baseUri: String): String = baseUri + TEMPLATE_URL + type.name.lowercase() + JSON_FORMAT
49+
50+
private fun parseTemplates(response: String): List<Template> {
51+
val data = JSONObject(response).getJSONObject(NODE_OCS).getJSONArray(NODE_DATA)
52+
53+
return (0 until data.length()).map { index ->
54+
data.getJSONObject(index).toTemplate()
55+
}
56+
}
57+
58+
private fun JSONObject.toTemplate(): Template = Template(
59+
getLong(NODE_ID),
60+
getString(NODE_NAME),
61+
optString(NODE_PREVIEW),
62+
Template.Type.parse(getString(NODE_TYPE)),
63+
getString(NODE_EXTENSION)
64+
)
65+
66+
companion object {
67+
private val TAG = FetchTemplateOperation::class.java.simpleName
68+
private const val SYNC_READ_TIMEOUT = 40000
69+
private const val SYNC_CONNECTION_TIMEOUT = 5000
70+
private const val TEMPLATE_URL = "/ocs/v2.php/apps/richdocuments/api/v1/templates/"
71+
private const val JSON_FORMAT = "?format=json"
72+
73+
private const val NODE_OCS = "ocs"
74+
private const val NODE_DATA = "data"
75+
private const val NODE_ID = "id"
76+
private const val NODE_NAME = "name"
77+
private const val NODE_PREVIEW = "preview"
78+
private const val NODE_TYPE = "type"
79+
private const val NODE_EXTENSION = "extension"
80+
}
81+
}

app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt

Lines changed: 43 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ class ChooseTemplateDialogFragment :
6565
Injectable {
6666

6767
private lateinit var fileNames: MutableSet<String>
68+
private var hasUserInteracted = false
6869

6970
@Inject
7071
lateinit var clientFactory: ClientFactory
@@ -142,6 +143,7 @@ class ChooseTemplateDialogFragment :
142143
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit
143144
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) = Unit
144145
override fun afterTextChanged(s: Editable) {
146+
hasUserInteracted = true
145147
checkFileNameAfterEachType()
146148
}
147149
})
@@ -224,71 +226,59 @@ class ChooseTemplateDialogFragment :
224226
private fun getOCCapability(): OCCapability = fileDataStorageManager.getCapability(currentAccount.user.accountName)
225227

226228
override fun onClick(v: View) {
229+
val selectedTemplate = adapter?.selectedTemplate
230+
?: return DisplayUtils.showSnackMessage(binding.list, R.string.select_one_template)
231+
232+
val state = resolveFilenameState()
233+
if (state !is TemplateFilenameState.Valid) {
234+
state.errorMessage?.let { DisplayUtils.showSnackMessage(requireActivity(), it.toString()) }
235+
return
236+
}
237+
227238
val name = binding.filename.text.toString()
228239
val path = parentFolder?.remotePath + name
229-
val selectedTemplate = adapter?.selectedTemplate
240+
val fullPath = if (name.endsWith(selectedTemplate.extension)) {
241+
path
242+
} else {
243+
path + DOT + selectedTemplate.extension
244+
}
230245

231-
val errorMessage = FileNameValidator.checkFileName(name, getOCCapability(), requireContext())
246+
createFromTemplate(selectedTemplate, fullPath)
247+
}
232248

233-
when {
234-
selectedTemplate == null -> {
235-
DisplayUtils.showSnackMessage(binding.list, R.string.select_one_template)
236-
}
249+
private fun resolveFilenameState(): TemplateFilenameState {
250+
val selectedTemplate = adapter?.selectedTemplate ?: return TemplateFilenameState.NoTemplateSelected
251+
val name = binding.filename.text.toString().trim()
252+
val validationError = FileNameValidator.checkFileName(name, getOCCapability(), requireContext(), fileNames)
237253

238-
errorMessage != null -> {
239-
DisplayUtils.showSnackMessage(requireActivity(), errorMessage)
240-
}
254+
return when {
255+
name.equals(DOT + selectedTemplate.extension, ignoreCase = true) ->
256+
TemplateFilenameState.JustExtension(getString(R.string.enter_filename))
241257

242-
name.equals(DOT + selectedTemplate.extension, ignoreCase = true) -> {
243-
DisplayUtils.showSnackMessage(binding.list, R.string.enter_filename)
244-
}
258+
validationError != null -> TemplateFilenameState.Invalid(validationError)
245259

246-
else -> {
247-
val fullPath = if (!name.endsWith(selectedTemplate.extension)) {
248-
path + DOT + selectedTemplate.extension
249-
} else {
250-
path
251-
}
252-
createFromTemplate(selectedTemplate, fullPath)
253-
}
260+
FileNameValidator.isFileHidden(name) ->
261+
TemplateFilenameState.HiddenName(getText(R.string.hidden_file_name_warning))
262+
263+
name.substringAfterLast(DOT) != selectedTemplate.extension ->
264+
TemplateFilenameState.ChangedExtension(getString(R.string.extension_cannot_be_changed))
265+
266+
else -> TemplateFilenameState.Valid
254267
}
255268
}
256269

257270
private fun checkFileNameAfterEachType() {
258-
if (positiveButton == null) return
271+
val positiveButton = positiveButton ?: return
272+
val state = resolveFilenameState()
259273

260-
val selectedTemplate = adapter?.selectedTemplate
261-
val name = binding.filename.text.toString().trim()
262-
val isNameJustExtension = selectedTemplate != null &&
263-
name.equals(
264-
DOT + selectedTemplate.extension,
265-
ignoreCase = true
266-
)
267-
val fileNameValidatorResult =
268-
FileNameValidator.checkFileName(name, getOCCapability(), requireContext(), fileNames)
269-
270-
val errorMessage = when {
271-
isNameJustExtension -> null
272-
fileNameValidatorResult != null -> fileNameValidatorResult
273-
else -> null
274-
}
274+
val isValid = state is TemplateFilenameState.Valid
275+
positiveButton.isEnabled = isValid
276+
positiveButton.isClickable = isValid
275277

276-
val isNameValid = (errorMessage == null) && !name.equals(DOT + selectedTemplate?.extension, ignoreCase = true)
277-
val isHiddenFileName = FileNameValidator.isFileHidden(name)
278-
val isChangedExtension = name.substringAfterLast(DOT) != selectedTemplate?.extension
278+
if (!hasUserInteracted) return
279279

280-
binding.filenameContainer.isErrorEnabled = !isNameValid || isHiddenFileName || isChangedExtension
281-
binding.filenameContainer.error = when {
282-
!isNameValid -> errorMessage ?: getString(R.string.enter_filename)
283-
isHiddenFileName -> getText(R.string.hidden_file_name_warning)
284-
isChangedExtension -> getString(R.string.extension_cannot_be_changed)
285-
else -> null
286-
}
287-
288-
positiveButton?.apply {
289-
isEnabled = isNameValid && !isHiddenFileName && !isChangedExtension
290-
isClickable = isEnabled
291-
}
280+
binding.filenameContainer.isErrorEnabled = state.errorMessage != null
281+
binding.filenameContainer.error = state.errorMessage
292282
}
293283

294284
@Suppress("LongParameterList", "DEPRECATION")
@@ -406,7 +396,8 @@ class ChooseTemplateDialogFragment :
406396
}
407397

408398
if (templateList.templates.isEmpty()) {
409-
DisplayUtils.showSnackMessage(fragment.binding.list, R.string.error_retrieving_templates)
399+
fragment.dismiss()
400+
DisplayUtils.showSnackMessage(fragment.requireActivity(), R.string.error_retrieving_templates)
410401
return
411402
}
412403

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
package com.owncloud.android.ui.dialog
8+
9+
sealed class TemplateFilenameState(val errorMessage: CharSequence?) {
10+
data object Valid : TemplateFilenameState(null)
11+
data object NoTemplateSelected : TemplateFilenameState(null)
12+
class JustExtension(message: CharSequence) : TemplateFilenameState(message)
13+
class HiddenName(message: CharSequence) : TemplateFilenameState(message)
14+
class ChangedExtension(message: CharSequence) : TemplateFilenameState(message)
15+
class Invalid(message: CharSequence) : TemplateFilenameState(message)
16+
}

0 commit comments

Comments
 (0)