diff --git a/app/src/androidTest/java/com/nextcloud/utils/FileNameValidatorTests.kt b/app/src/androidTest/java/com/nextcloud/utils/FileNameValidatorTests.kt index 13f2e4585281..b973e482042e 100644 --- a/app/src/androidTest/java/com/nextcloud/utils/FileNameValidatorTests.kt +++ b/app/src/androidTest/java/com/nextcloud/utils/FileNameValidatorTests.kt @@ -23,6 +23,9 @@ import org.junit.Test @Suppress("TooManyFunctions") class FileNameValidatorTests : AbstractOnServerIT() { + companion object { + private const val MAX_FILENAME_SIZE = 250 + } private var capability: OCCapability = fileDataStorageManager.getCapability(account.name) @@ -109,6 +112,20 @@ class FileNameValidatorTests : AbstractOnServerIT() { assertEquals(targetContext.getString(R.string.filename_empty), result) } + @Test + fun testLongFileName() { + val filename = "a".repeat(MAX_FILENAME_SIZE) + val result = FileNameValidator.checkFileName(filename, capability, targetContext) + assertNull(result) + } + + @Test + fun testTooLongFileName() { + val filename = "a".repeat(MAX_FILENAME_SIZE + 1) + val result = FileNameValidator.checkFileName(filename, capability, targetContext) + assertEquals(targetContext.getString(R.string.filename_too_long), result) + } + @Test fun testFileAlreadyExists() { val existingFiles = setOf("existingFile") diff --git a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt index 7b96e925ae9a..36444224c6b2 100644 --- a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt +++ b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt @@ -22,6 +22,8 @@ import com.owncloud.android.datamodel.OCFile import com.owncloud.android.lib.resources.status.OCCapability object FileNameValidator { + // As defined, server-side, in FilenameValidator::validateFilename + private const val MAX_FILENAME_SIZE = 250 /** * Checks the validity of a file name. @@ -45,6 +47,10 @@ object FileNameValidator { return context.getString(R.string.filename_empty) } + if (filename.length > MAX_FILENAME_SIZE) { + return context.getString(R.string.filename_too_long) + } + if (isFileNameAlreadyExist(filename, existingFileNames)) { return context.getString(R.string.file_already_exists) } 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 3c87d97dc6b6..0feae1dc60e5 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 @@ -19,8 +19,10 @@ import android.os.Bundle import android.text.Editable import android.text.TextWatcher import android.view.View +import androidx.annotation.WorkerThread 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 @@ -31,10 +33,10 @@ 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.ResultParser.data import com.nextcloud.utils.SnackbarUtil import com.nextcloud.utils.extensions.getParcelableArgument import com.nextcloud.utils.fileNameValidator.FileNameValidator -import com.owncloud.android.MainApp import com.owncloud.android.R import com.owncloud.android.databinding.ChooseTemplateBinding import com.owncloud.android.datamodel.FileDataStorageManager @@ -53,6 +55,9 @@ import com.owncloud.android.ui.adapter.TemplateAdapter import com.owncloud.android.utils.FileStorageUtils import com.owncloud.android.utils.KeyboardUtils import com.owncloud.android.utils.theme.ViewThemeUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.lang.ref.WeakReference import javax.inject.Inject @@ -195,7 +200,62 @@ class ChooseTemplateDialogFragment : } private fun createFromTemplate(template: Template, path: String) { - CreateFileFromTemplateTask(this, clientFactory, currentAccount.user, template, path, creator).execute() + lifecycleScope.launch { + val result = withContext(Dispatchers.IO) { createFileOnServer(template, path) } + + val binding = _binding + if (!isAdded || binding == null) { + Log_OC.w(TAG, "Dialog no longer attached, ignoring create from template result") + return@launch + } + + when (result) { + is CreateFileFromTemplateResult.Success -> openEditor(result) + else -> result.showError(binding.list) + } + } + } + + @WorkerThread + private fun createFileOnServer(template: Template, path: String): CreateFileFromTemplateResult { + return try { + val user = currentAccount.user + + val createResult = DirectEditingCreateFileRemoteOperation(path, creator?.editor, creator?.id, template.id) + .execute(clientFactory.createNextcloudClient(user)) + val editorUrl = createResult.resultData?.takeIf { createResult.isSuccess } + ?: return CreateFileFromTemplateResult.FailedCreateFile + + readAndStoreCreatedFile(user, path, editorUrl) + } catch (e: CreationException) { + Log_OC.e(TAG, "Error creating file from template!", e) + CreateFileFromTemplateResult.FailedCreateFromTemplate + } + } + + @WorkerThread + private fun readAndStoreCreatedFile(user: User, path: String, editorUrl: String): CreateFileFromTemplateResult { + val remoteFile = ReadFileRemoteOperation(path) + .execute(clientFactory.create(user)) + .data(RemoteFile::class.java) + ?: return CreateFileFromTemplateResult.FailedReadFile + + fileDataStorageManager.saveFile(FileStorageUtils.fillOCFile(remoteFile)) + return fileDataStorageManager.getFileByPath(path) + ?.let { CreateFileFromTemplateResult.Success(editorUrl, it) } + ?: CreateFileFromTemplateResult.FailedSaveLocalFile + } + + private fun openEditor(result: CreateFileFromTemplateResult.Success) { + val editorWebView = Intent(requireContext(), TextEditorWebView::class.java).apply { + putExtra(ExternalSiteWebView.EXTRA_TITLE, "Text") + putExtra(ExternalSiteWebView.EXTRA_URL, result.url) + putExtra(FileActivity.EXTRA_FILE, result.file) + putExtra(ExternalSiteWebView.EXTRA_SHOW_SIDEBAR, false) + } + + startActivity(editorWebView) + dismiss() } @SuppressLint("NotifyDataSetChanged") @@ -282,80 +342,25 @@ class ChooseTemplateDialogFragment : binding.filenameContainer.error = state.errorMessage } - @Suppress("LongParameterList", "DEPRECATION") - private class CreateFileFromTemplateTask( - chooseTemplateDialogFragment: ChooseTemplateDialogFragment, - private val clientFactory: ClientFactory?, - private val user: User, - private val template: Template, - private val path: String, - private val creator: Creator? - ) : AsyncTask() { - private val chooseTemplateDialogFragmentWeakReference: WeakReference = - WeakReference(chooseTemplateDialogFragment) - private var file: OCFile? = null - - @Deprecated("Deprecated in Java") - @Suppress("ReturnCount") // legacy code - override fun doInBackground(vararg params: Void): String { - return try { - val client = clientFactory?.create(user) ?: return "" - val nextcloudClient = clientFactory.createNextcloudClient(user) - val result = DirectEditingCreateFileRemoteOperation( - path, - creator?.editor, - creator?.id, - template.id - ).execute(nextcloudClient) - if (!result.isSuccess) { - return "" - } - val newFileResult = ReadFileRemoteOperation(path).execute(client) - if (!newFileResult.isSuccess) { - return "" - } - val fragment = chooseTemplateDialogFragmentWeakReference.get() ?: return "" - val context = fragment.context - ?: // fragment has been detached - return "" - val storageManager = FileDataStorageManager( - user, - context.contentResolver - ) - val temp = FileStorageUtils.fillOCFile(newFileResult.data[0] as RemoteFile) - storageManager.saveFile(temp) - file = storageManager.getFileByPath(path) - result.resultData - } catch (e: CreationException) { - Log_OC.e(TAG, "Error creating file from template!", e) - "" - } - } + sealed class CreateFileFromTemplateResult { + data class Success(val url: String, val file: OCFile) : CreateFileFromTemplateResult() - @Deprecated("Deprecated in Java") - override fun onPostExecute(url: String) { - val fragment = chooseTemplateDialogFragmentWeakReference.get() - if (fragment == null || !fragment.isAdded) { - Log_OC.e(TAG, "Error creating file from template!") - return - } + data object FailedCreateFile : CreateFileFromTemplateResult() + data object FailedCreateFromTemplate : CreateFileFromTemplateResult() + data object FailedReadFile : CreateFileFromTemplateResult() + data object FailedSaveLocalFile : CreateFileFromTemplateResult() - if (url.isEmpty()) { - SnackbarUtil.show(fragment.binding.list, R.string.error_creating_file_from_template) - return - } + fun showError(view: View) { + if (this is Success) return - val editorWebView = Intent(MainApp.getAppContext(), TextEditorWebView::class.java).apply { - putExtra(ExternalSiteWebView.EXTRA_TITLE, "Text") - putExtra(ExternalSiteWebView.EXTRA_URL, url) - putExtra(FileActivity.EXTRA_FILE, file) - putExtra(ExternalSiteWebView.EXTRA_SHOW_SIDEBAR, false) + val messageId = when (this) { + FailedCreateFile -> R.string.error_creating_file + FailedCreateFromTemplate -> R.string.error_creating_file_from_template + FailedReadFile -> R.string.error_reading_file + FailedSaveLocalFile -> R.string.error_saving_local_file } - fragment.run { - startActivity(editorWebView) - dismiss() - } + SnackbarUtil.show(view, messageId) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fa3557334b9e..c18bcf8427e0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -486,6 +486,7 @@ Forbidden characters: / \\ < > : " | ? * Filename contains at least one invalid character Filename cannot be empty + Filename cannot be longer than 250 characters Folder name cannot be empty Label cannot be empty Wait a moment… @@ -1389,7 +1390,10 @@ Type in the search bar above to find files, contacts, calendar events, and more across your account. Found no images or videos + Error creating file Error creating file from template + Error reading file + Error saving local file No app available for sending the selected files All files access Media read-only