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 919eb152c3ea..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 @@ -1,7 +1,7 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-License-Identifier: AGPL-3.0-or-later */ package com.owncloud.android.ui.dialog @@ -10,23 +10,22 @@ import android.annotation.SuppressLint import android.app.Activity import android.app.Dialog import android.content.Intent -import android.os.AsyncTask import android.os.Bundle import android.text.Editable import android.text.TextWatcher import android.view.View import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager import com.google.android.material.button.MaterialButton import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.common.collect.Sets import com.nextcloud.client.account.CurrentAccountProvider import com.nextcloud.client.account.User import com.nextcloud.client.di.Injectable import com.nextcloud.client.network.ClientFactory -import com.nextcloud.client.network.ClientFactory.CreationException import com.nextcloud.utils.extensions.getParcelableArgument +import com.nextcloud.utils.extensions.getTypedActivity import com.nextcloud.utils.fileNameValidator.FileNameValidator import com.owncloud.android.MainApp import com.owncloud.android.R @@ -36,10 +35,10 @@ import com.owncloud.android.datamodel.OCFile import com.owncloud.android.datamodel.Template import com.owncloud.android.files.CreateFileFromTemplateOperation import com.owncloud.android.files.FetchTemplateOperation -import com.owncloud.android.lib.common.OwnCloudClient import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation import com.owncloud.android.lib.resources.files.model.RemoteFile +import com.owncloud.android.ui.activity.BaseActivity import com.owncloud.android.ui.activity.ExternalSiteWebView import com.owncloud.android.ui.activity.RichDocumentsEditorWebView import com.owncloud.android.ui.adapter.RichDocumentsTemplateAdapter @@ -49,18 +48,20 @@ import com.owncloud.android.utils.FileStorageUtils import com.owncloud.android.utils.KeyboardUtils import com.owncloud.android.utils.NextcloudServer import com.owncloud.android.utils.theme.ViewThemeUtils -import java.lang.ref.WeakReference +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject -/** - * Dialog to show templates for new documents/spreadsheets/presentations. - */ +@Suppress("TooManyFunctions") class ChooseRichDocumentsTemplateDialogFragment : DialogFragment(), View.OnClickListener, RichDocumentsTemplateAdapter.ClickListener, Injectable { - private var fileNames: MutableSet? = null + + private var fileNames: Set = emptySet() + private var hasUserInteracted = false @Inject lateinit var currentAccount: CurrentAccountProvider @@ -79,7 +80,6 @@ class ChooseRichDocumentsTemplateDialogFragment : private var adapter: RichDocumentsTemplateAdapter? = null private var parentFolder: OCFile? = null - private var client: OwnCloudClient? = null private var positiveButton: MaterialButton? = null private var waitDialog: DialogFragment? = null @@ -129,12 +129,14 @@ class ChooseRichDocumentsTemplateDialogFragment : val arguments = arguments ?: throw IllegalArgumentException("Arguments may not be null") val activity = activity ?: throw IllegalArgumentException("Activity may not be null") - initClient() initFilenames(arguments) viewThemeUtils.material.colorTextInputLayout(binding.filenameContainer) val type = Type.valueOf(arguments.getString(ARG_TYPE) ?: "") - FetchTemplateTask(this, client).execute(type) + + lifecycleScope.launch { + fetchTemplate(type) + } initList(type) addTextChangeListener() @@ -144,23 +146,11 @@ class ChooseRichDocumentsTemplateDialogFragment : return builder.create() } - @Suppress("DEPRECATION", "TooGenericExceptionThrown") - private fun initClient() { - try { - client = clientFactory.create(currentAccount.user) - } catch (e: CreationException) { - throw RuntimeException(e) - } - } - private fun initFilenames(arguments: Bundle) { parentFolder = arguments.getParcelableArgument(ARG_PARENT_FOLDER, OCFile::class.java) - val folderContent = fileDataStorageManager.getFolderContent(parentFolder, false) - fileNames = Sets.newHashSetWithExpectedSize(folderContent.size) - - for (file in folderContent) { - fileNames?.add(file.fileName) - } + fileNames = fileDataStorageManager + .getFolderContent(parentFolder, false) + .mapTo(HashSet()) { it.fileName } } private fun initList(type: Type) { @@ -181,6 +171,7 @@ class ChooseRichDocumentsTemplateDialogFragment : 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 checkEnablingCreateButton() } }) @@ -217,7 +208,10 @@ class ChooseRichDocumentsTemplateDialogFragment : waitDialog = newInstance(R.string.wait_a_moment, false).also { it.show(parentFragmentManager, WAIT_DIALOG_TAG) } - CreateFileFromTemplateTask(this, client, template, path, currentAccount.user).execute() + + lifecycleScope.launch { + createFileFromTemplate(template, path, currentAccount.user) + } } @SuppressLint("NotifyDataSetChanged") @@ -242,28 +236,28 @@ class ChooseRichDocumentsTemplateDialogFragment : } override fun onClick(v: View) { - val name = fileNameText - val path = parentFolder?.remotePath + name - val selectedTemplate = adapter?.selectedTemplate + ?: return DisplayUtils.showSnackMessage(binding.list, R.string.select_one_template) - val errorMessage = FileNameValidator.checkFileName( - name, - fileDataStorageManager.getCapability(currentAccount.user), - requireContext(), - fileNames ?: setOf() - ) + val state = resolveFilenameState() + when (state) { + is FilenameState.Invalid -> + DisplayUtils.showSnackMessage(requireActivity(), state.errorMessage) + + is FilenameState.JustExtension -> + DisplayUtils.showSnackMessage(binding.list, R.string.enter_filename) - if (selectedTemplate == null) { - DisplayUtils.showSnackMessage(binding.list, R.string.select_one_template) - } else if (errorMessage != null) { - DisplayUtils.showSnackMessage(requireActivity(), errorMessage) - } else if (name.equals(DOT + selectedTemplate.extension, ignoreCase = true)) { - DisplayUtils.showSnackMessage(binding.list, R.string.enter_filename) - } else if (!name.endsWith(selectedTemplate.extension)) { - createFromTemplate(selectedTemplate, path + DOT + selectedTemplate.extension) - } else { - createFromTemplate(selectedTemplate, path) + is FilenameState.Valid -> { + val name = fileNameText + val path = parentFolder?.remotePath + name + if (!name.endsWith(selectedTemplate.extension)) { + createFromTemplate(selectedTemplate, path + DOT + selectedTemplate.extension) + } else { + createFromTemplate(selectedTemplate, path) + } + } + + else -> Unit } } @@ -284,186 +278,159 @@ class ChooseRichDocumentsTemplateDialogFragment : binding.filename.setText(String.format("%s.%s", template.name, template.extension)) } - val dotIndex = fileNameText.lastIndexOf('.') + val dotIndex = binding.filename.text?.lastIndexOf('.') ?: -1 if (dotIndex >= 0) { binding.filename.setSelection(dotIndex) } } - private fun checkEnablingCreateButton() { - if (positiveButton == null) { - return - } + // region filename state + private sealed class FilenameState(val isError: Boolean, val errorMessage: String?) { + data object Valid : FilenameState(isError = false, errorMessage = null) + data object NoTemplateSelected : FilenameState(isError = false, errorMessage = null) + class JustExtension(message: String) : FilenameState(isError = true, errorMessage = message) + class ChangedExtension(message: String) : FilenameState(isError = true, errorMessage = message) + class Invalid(message: String) : FilenameState(isError = true, errorMessage = message) + } - val selectedTemplate = adapter?.selectedTemplate + private fun resolveFilenameState(): FilenameState { + val selectedTemplate = adapter?.selectedTemplate ?: return FilenameState.NoTemplateSelected val name = fileNameText + val errorMessage = FileNameValidator.checkFileName( name, fileDataStorageManager.getCapability(currentAccount.user), requireContext(), fileNames ?: setOf() ) - val isExtension = ( - selectedTemplate == null || - !name.equals( - DOT + selectedTemplate.extension, - ignoreCase = true - ) - ) - val isChangedExtension = name.substringAfterLast(DOT) != selectedTemplate?.extension - - val isEnable = isExtension && !isChangedExtension && errorMessage == null - - positiveButton?.let { - it.isEnabled = isEnable - it.isClickable = isEnable - } - binding.filenameContainer.run { - isErrorEnabled = !isEnable - error = if (!isEnable) { - when { - errorMessage != null -> errorMessage - isChangedExtension -> getString(R.string.extension_cannot_be_changed) - else -> getText(R.string.filename_empty) - } - } else { - null - } - } - } + return when { + errorMessage != null -> FilenameState.Invalid(errorMessage) - @Suppress("DEPRECATION") - private class CreateFileFromTemplateTask( - chooseTemplateDialogFragment: ChooseRichDocumentsTemplateDialogFragment?, - private val client: OwnCloudClient?, - private val template: Template, - private val path: String, - private val user: User - ) : AsyncTask() { - private val chooseTemplateDialogFragmentWeakReference = WeakReference(chooseTemplateDialogFragment) - private var file: OCFile? = null - - @Suppress("ReturnCount") - @Deprecated("Deprecated in Java") - override fun doInBackground(vararg voids: Void?): String { - val result = CreateFileFromTemplateOperation(path, template.id).execute(client) - - if (!result.isSuccess) { - return "" - } - - // get file - val newFileResult = ReadFileRemoteOperation(path).execute(client) + name.equals(DOT + selectedTemplate.extension, ignoreCase = true) -> + FilenameState.JustExtension(getString(R.string.filename_empty)) - if (!newFileResult.isSuccess) { - return "" - } + name.contains(DOT) && + name.substringAfterLast(DOT).isNotEmpty() && + name.substringAfterLast(DOT) != selectedTemplate.extension -> + FilenameState.ChangedExtension(getString(R.string.extension_cannot_be_changed)) - val temp = FileStorageUtils.fillOCFile(newFileResult.data[0] as RemoteFile) + else -> FilenameState.Valid + } + } - if (chooseTemplateDialogFragmentWeakReference.get() == null) { - return "" - } + private fun checkEnablingCreateButton() { + val positiveButton = positiveButton ?: return + val state = resolveFilenameState() - val storageManager = FileDataStorageManager( - user, - chooseTemplateDialogFragmentWeakReference.get()!!.requireContext().contentResolver - ) - storageManager.saveFile(temp) - file = storageManager.getFileByPath(path) + val isEnabled = state is FilenameState.Valid + positiveButton.isEnabled = isEnabled + positiveButton.isClickable = isEnabled - return result.data[0].toString() - } + if (!hasUserInteracted) return - @Deprecated("Deprecated in Java") - override fun onPostExecute(url: String) { - val fragment = chooseTemplateDialogFragmentWeakReference.get() + binding.filenameContainer.isErrorEnabled = state.isError + binding.filenameContainer.error = state.errorMessage + } + // endregion - if (fragment == null || !fragment.isAdded) { - Log_OC.e(TAG, "Error creating file from template!") - return - } + // region remote operations + @Suppress("DEPRECATION") + private suspend fun createFileFromTemplate(template: Template, path: String, user: User) = + withContext(Dispatchers.IO) { + val activity = getTypedActivity(BaseActivity::class.java) + val client = activity?.clientRepository?.getOwncloudClient() ?: return@withContext + + val url = CreateFileFromTemplateOperation(path, template.id) + .execute(client) + .takeIf { it.isSuccess } + ?.data?.get(0)?.toString() + ?: run { + showErrorOnMain(R.string.error_creating_file_from_template) + return@withContext + } - fragment.waitDialog?.dismiss() + val file = ReadFileRemoteOperation(path) + .execute(client) + .takeIf { it.isSuccess } + ?.data?.get(0) + ?.let { FileStorageUtils.fillOCFile(it as RemoteFile) } + ?.also { FileDataStorageManager(user, requireContext().contentResolver).saveFile(it) } + ?.let { FileDataStorageManager(user, requireContext().contentResolver).getFileByPath(path) } + ?: run { + showErrorOnMain(R.string.error_creating_file_from_template) + return@withContext + } - if (url.isEmpty()) { - fragment.dismiss() - DisplayUtils.showSnackMessage( - fragment.requireActivity(), - R.string.error_creating_file_from_template - ) + withContext(Dispatchers.Main) { + if (!isAdded) { + Log_OC.e(TAG, "Error creating file from template!") + return@withContext + } - return - } + waitDialog?.dismiss() - val intent = Intent(MainApp.getAppContext(), RichDocumentsEditorWebView::class.java).apply { - putExtra(ExternalSiteWebView.EXTRA_TITLE, "Collabora") - putExtra(ExternalSiteWebView.EXTRA_URL, url) - putExtra(ExternalSiteWebView.EXTRA_FILE, file) - putExtra(ExternalSiteWebView.EXTRA_SHOW_SIDEBAR, false) - putExtra(ExternalSiteWebView.EXTRA_TEMPLATE, template) - } + val intent = Intent(MainApp.getAppContext(), RichDocumentsEditorWebView::class.java).apply { + putExtra(ExternalSiteWebView.EXTRA_TITLE, "Collabora") + putExtra(ExternalSiteWebView.EXTRA_URL, url) + putExtra(ExternalSiteWebView.EXTRA_FILE, file) + putExtra(ExternalSiteWebView.EXTRA_SHOW_SIDEBAR, false) + putExtra(ExternalSiteWebView.EXTRA_TEMPLATE, template) + } - fragment.run { startActivity(intent) dismiss() } } + + private suspend fun showErrorOnMain(stringRes: Int) = withContext(Dispatchers.Main) { + if (!isAdded) return@withContext + waitDialog?.dismiss() + dismiss() + DisplayUtils.showSnackMessage(requireActivity(), stringRes) } @Suppress("DEPRECATION") - private class FetchTemplateTask( - chooseTemplateDialogFragment: ChooseRichDocumentsTemplateDialogFragment, - private val client: OwnCloudClient? - ) : AsyncTask>() { - private val chooseTemplateDialogFragmentWeakReference = WeakReference(chooseTemplateDialogFragment) - - @Deprecated("Deprecated in Java") - override fun doInBackground(vararg type: Type?): List