Skip to content

Commit c006da2

Browse files
PhilLabalperozturk96
authored andcommitted
Added FileNameTextWatcher to wrap the FileNameValidator for text fields
Deduplicating the code in ReceiveExternalFilesActivity and RenameFileDialogFragment. Also clarified the DialogFragmentIT, which tests the RenameFileDialogFragment, by making the function used more explicit. Signed-off-by: Philipp Hasper <vcs@hasper.info>
1 parent 5084551 commit c006da2

5 files changed

Lines changed: 130 additions & 116 deletions

File tree

app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.kt

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,6 @@ import com.owncloud.android.lib.resources.status.OwnCloudVersion
6363
import com.owncloud.android.lib.resources.users.Status
6464
import com.owncloud.android.lib.resources.users.StatusType
6565
import com.owncloud.android.ui.activity.FileDisplayActivity
66-
import com.owncloud.android.ui.dialog.LoadingDialog.Companion.newInstance
67-
import com.owncloud.android.ui.dialog.RenameFileDialogFragment.Companion.newInstance
68-
import com.owncloud.android.ui.dialog.SharePasswordDialogFragment.Companion.newInstance
69-
import com.owncloud.android.ui.dialog.SslUntrustedCertDialog.Companion.newInstanceForEmptySslError
7066
import com.owncloud.android.ui.fragment.OCFileListBottomSheetActions
7167
import com.owncloud.android.ui.fragment.OCFileListBottomSheetDialog
7268
import com.owncloud.android.ui.fragment.ProfileBottomSheetDialog
@@ -104,7 +100,7 @@ class DialogFragmentIT : AbstractIT() {
104100
Looper.prepare()
105101
}
106102

107-
newInstance(
103+
RenameFileDialogFragment.newInstance(
108104
OCFile("/Test/"),
109105
OCFile("/")
110106
).run {
@@ -115,7 +111,7 @@ class DialogFragmentIT : AbstractIT() {
115111
@Test
116112
@ScreenshotTest
117113
fun testLoadingDialog() {
118-
newInstance("Wait…").run {
114+
LoadingDialog.newInstance("Wait…").run {
119115
showDialog(this)
120116
}
121117
}
@@ -240,7 +236,7 @@ class DialogFragmentIT : AbstractIT() {
240236
if (Looper.myLooper() == null) {
241237
Looper.prepare()
242238
}
243-
val sut = newInstance(OCFile("/"), true, false)
239+
val sut = SharePasswordDialogFragment.newInstance(OCFile("/"), createShare = true, askForPassword = false)
244240
showDialog(sut)
245241
}
246242

@@ -250,7 +246,7 @@ class DialogFragmentIT : AbstractIT() {
250246
if (Looper.myLooper() == null) {
251247
Looper.prepare()
252248
}
253-
val sut = newInstance(OCFile("/"), true, true)
249+
val sut = SharePasswordDialogFragment.newInstance(OCFile("/"), createShare = true, askForPassword = true)
254250
showDialog(sut)
255251
}
256252

@@ -634,7 +630,7 @@ class DialogFragmentIT : AbstractIT() {
634630

635631
val handler = mockk<SslErrorHandler>(relaxed = true)
636632

637-
newInstanceForEmptySslError(sslError, handler).run {
633+
SslUntrustedCertDialog.newInstanceForEmptySslError(sslError, handler).run {
638634
showDialog(this)
639635
}
640636
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Philipp Hasper <vcs@hasper.info>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
package com.nextcloud.utils.fileNameValidator
9+
10+
import android.content.Context
11+
import android.text.Editable
12+
import android.text.TextWatcher
13+
import androidx.core.util.Consumer
14+
import com.nextcloud.utils.fileNameValidator.FileNameValidator.checkFileName
15+
import com.nextcloud.utils.fileNameValidator.FileNameValidator.isExtensionChanged
16+
import com.nextcloud.utils.fileNameValidator.FileNameValidator.isFileHidden
17+
import com.owncloud.android.R
18+
import com.owncloud.android.lib.resources.status.OCCapability
19+
20+
/**
21+
* A TextWatcher which wraps around [FileNameValidator]
22+
*/
23+
@Suppress("LongParameterList")
24+
class FileNameTextWatcher(
25+
private val previousFileName: String?,
26+
private val context: Context,
27+
private val getCapabilities: () -> OCCapability,
28+
private val getExistingFileNames: () -> Set<String>?,
29+
private val onError: Consumer<String>,
30+
private val onWarning: Consumer<String>,
31+
private val onOkay: Runnable
32+
) : TextWatcher {
33+
34+
private var isOkay: Boolean = true // Used to trigger the onOkay callback only once
35+
36+
override fun afterTextChanged(s: Editable?) {
37+
var newFileName = ""
38+
if (s != null) {
39+
newFileName = s.toString()
40+
}
41+
42+
val errorMessage = checkFileName(newFileName, getCapabilities(), context, getExistingFileNames())
43+
44+
if (isFileHidden(newFileName)) {
45+
isOkay = false
46+
onWarning.accept(context.getString(R.string.hidden_file_name_warning))
47+
} else if (errorMessage != null) {
48+
isOkay = false
49+
onError.accept(errorMessage)
50+
} else if (isExtensionChanged(previousFileName, newFileName)) {
51+
isOkay = false
52+
onWarning.accept(context.getString(R.string.warn_rename_extension))
53+
} else if (!isOkay) {
54+
isOkay = true
55+
onOkay.run()
56+
}
57+
}
58+
59+
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
60+
61+
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
62+
}

app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import com.nextcloud.utils.extensions.removeFileExtension
1919
import com.owncloud.android.R
2020
import com.owncloud.android.datamodel.OCFile
2121
import com.owncloud.android.lib.resources.status.OCCapability
22+
import java.io.File
2223

2324
object FileNameValidator {
2425

@@ -29,22 +30,22 @@ object FileNameValidator {
2930
* @param capability The capabilities affecting the validation criteria
3031
* such as forbiddenFilenames, forbiddenCharacters.
3132
* @param context The context used for retrieving error messages.
32-
* @param existedFileNames Set of existing file names to avoid duplicates.
33+
* @param existingFileNames Set of existing file names to avoid duplicates.
3334
* @return An error message if the filename is invalid, null otherwise.
3435
*/
3536
@Suppress("ReturnCount", "NestedBlockDepth")
3637
fun checkFileName(
3738
filename: String,
3839
capability: OCCapability,
3940
context: Context,
40-
existedFileNames: Set<String>? = null
41+
existingFileNames: Set<String>? = null
4142
): String? {
4243
if (filename.isBlank()) {
4344
return context.getString(R.string.filename_empty)
4445
}
4546

46-
existedFileNames?.let {
47-
if (isFileNameAlreadyExist(filename, existedFileNames)) {
47+
existingFileNames?.let {
48+
if (isFileNameAlreadyExist(filename, existingFileNames)) {
4849
return context.getString(R.string.file_already_exists)
4950
}
5051
}
@@ -147,6 +148,19 @@ object FileNameValidator {
147148
return null
148149
}
149150

151+
/**
152+
* @return True, if the extension of both filenames is different. If either filename is null, function returns false
153+
*/
154+
fun isExtensionChanged(previousFileName: String?, newFileName: String?): Boolean {
155+
if (previousFileName == null || newFileName == null) {
156+
return false
157+
}
158+
val previousExtension = File(previousFileName).extension
159+
val newExtension = File(newFileName).extension
160+
161+
return previousExtension != newExtension
162+
}
163+
150164
fun isFileHidden(name: String): Boolean = !TextUtils.isEmpty(name) && name[0] == '.'
151165

152166
fun isFileNameAlreadyExist(name: String, fileNames: Set<String>): Boolean = fileNames.contains(name)

app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java

Lines changed: 22 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,7 @@
3030
import android.os.Handler;
3131
import android.os.Looper;
3232
import android.os.Parcelable;
33-
import android.text.Editable;
3433
import android.text.TextUtils;
35-
import android.text.TextWatcher;
3634
import android.text.format.DateFormat;
3735
import android.view.LayoutInflater;
3836
import android.view.Menu;
@@ -58,6 +56,7 @@
5856
import com.nextcloud.utils.extensions.BundleExtensionsKt;
5957
import com.nextcloud.utils.extensions.FileExtensionsKt;
6058
import com.nextcloud.utils.extensions.IntentExtensionsKt;
59+
import com.nextcloud.utils.fileNameValidator.FileNameTextWatcher;
6160
import com.nextcloud.utils.fileNameValidator.FileNameValidator;
6261
import com.owncloud.android.MainApp;
6362
import com.owncloud.android.R;
@@ -91,8 +90,6 @@
9190
import com.owncloud.android.utils.MimeType;
9291
import com.owncloud.android.utils.theme.ViewThemeUtils;
9392

94-
import org.apache.commons.io.FilenameUtils;
95-
9693
import java.io.File;
9794
import java.io.FileWriter;
9895
import java.io.IOException;
@@ -133,7 +130,7 @@
133130
public class ReceiveExternalFilesActivity extends FileActivity
134131
implements View.OnClickListener, CopyAndUploadContentUrisTask.OnCopyTmpFilesTaskListener,
135132
SortingOrderDialogFragment.OnSortingOrderListener, Injectable, AccountChooserInterface,
136-
ReceiveExternalFilesAdapter.OnItemClickListener, TextWatcher {
133+
ReceiveExternalFilesAdapter.OnItemClickListener {
137134

138135
private static final String TAG = ReceiveExternalFilesActivity.class.getSimpleName();
139136

@@ -334,53 +331,6 @@ public void selectFile(OCFile file) {
334331
}
335332
}
336333

337-
@Override
338-
public void afterTextChanged(Editable editable) {}
339-
340-
@Override
341-
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
342-
343-
// TODO: this is copy-paste from RenameFileDialogFragment.kt
344-
@Override
345-
public void onTextChanged(CharSequence s, int start, int before, int count) {
346-
final var newFileName = Objects.requireNonNullElse(binding.userInput.getText(), "").toString();
347-
final var positiveButton = binding.uploaderChooseFolder;
348-
349-
final var existingFiles = receiveExternalFilesAdapter.getFileNames();
350-
final var errorMessage = FileNameValidator.INSTANCE.checkFileName(newFileName, getCapabilities(), this, existingFiles);
351-
352-
if (FileNameValidator.INSTANCE.isFileHidden(newFileName)) {
353-
binding.userInputContainer.setError(getText(R.string.hidden_file_name_warning));
354-
positiveButton.setEnabled(true);
355-
} else if (errorMessage != null) {
356-
binding.userInputContainer.setError(errorMessage);
357-
positiveButton.setEnabled(false);
358-
} else if (checkExtensionRenamed(newFileName)) {
359-
binding.userInputContainer.setError(getText(R.string.warn_rename_extension));
360-
positiveButton.setEnabled(true);
361-
} else if (binding.userInputContainer.getError() != null) {
362-
binding.userInputContainer.setError(null);
363-
// Called to remove extra padding
364-
binding.userInputContainer.setErrorEnabled(false);
365-
positiveButton.setEnabled(true);
366-
}
367-
}
368-
369-
private boolean checkExtensionRenamed(@NonNull String newFileName) {
370-
if (mStreamsToUpload == null || mStreamsToUpload.size() != 1) {
371-
return false;
372-
}
373-
final String previousFileName = getDisplayNameForUri((Uri) mStreamsToUpload.get(0), getActivity());
374-
if (previousFileName == null) {
375-
return false;
376-
}
377-
378-
var previousExtension = FilenameUtils.getExtension(previousFileName);
379-
var newExtension = FilenameUtils.getExtension(newFileName);
380-
381-
return !Objects.equals(previousExtension, newExtension);
382-
}
383-
384334
public static class DialogNoAccount extends DialogFragment {
385335
private final ViewThemeUtils viewThemeUtils;
386336

@@ -912,7 +862,26 @@ private void setupFileNameInputField() {
912862

913863
binding.userInput.setVisibility(View.VISIBLE);
914864
binding.userInput.setText(userProvidedFileName.isEmpty() ? fileName : userProvidedFileName);
915-
binding.userInput.addTextChangedListener(this);
865+
binding.userInput.addTextChangedListener(new FileNameTextWatcher(
866+
fileName,
867+
this,
868+
this::getCapabilities,
869+
() -> receiveExternalFilesAdapter.getFileNames(),
870+
s -> {
871+
binding.userInputContainer.setError(s);
872+
binding.uploaderChooseFolder.setEnabled(false);
873+
},
874+
s -> {
875+
binding.userInputContainer.setError(s);
876+
binding.uploaderChooseFolder.setEnabled(true);
877+
},
878+
() -> {
879+
binding.userInputContainer.setError(null);
880+
binding.userInputContainer.setErrorEnabled(false);
881+
binding.uploaderChooseFolder.setEnabled(true);
882+
}
883+
));
884+
916885
mFileDisplayNameTransformer = uri ->
917886
Objects.requireNonNullElse(binding.userInput.getText(), fileName).toString();
918887

0 commit comments

Comments
 (0)