diff --git a/app/src/androidTest/java/com/nextcloud/test/NextcloudViewMatchers.kt b/app/src/androidTest/java/com/nextcloud/test/NextcloudViewMatchers.kt new file mode 100644 index 000000000000..11fa50fb90b0 --- /dev/null +++ b/app/src/androidTest/java/com/nextcloud/test/NextcloudViewMatchers.kt @@ -0,0 +1,31 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Philipp Hasper + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.test + +import android.view.View +import android.widget.TextView +import org.hamcrest.Description +import org.hamcrest.Matcher +import org.hamcrest.TypeSafeMatcher + +fun withSelectedText(expected: String): Matcher = object : TypeSafeMatcher() { + override fun describeTo(description: Description) { + description.appendText("with selected text \"$expected\"") + } + + @Suppress("ReturnCount") + override fun matchesSafely(view: View): Boolean { + if (view !is TextView) return false + val text = view.text?.toString() ?: "" + val s = view.selectionStart + val e = view.selectionEnd + @Suppress("ComplexCondition") + if (s < 0 || e < 0 || s > e || e > text.length) return false + return text.substring(s, e) == expected + } +} diff --git a/app/src/androidTest/java/com/owncloud/android/AbstractIT.java b/app/src/androidTest/java/com/owncloud/android/AbstractIT.java index 5ea27541062f..4b925618767c 100644 --- a/app/src/androidTest/java/com/owncloud/android/AbstractIT.java +++ b/app/src/androidTest/java/com/owncloud/android/AbstractIT.java @@ -68,7 +68,9 @@ import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; +import java.util.Arrays; import java.util.Collection; +import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.Optional; @@ -234,7 +236,7 @@ protected Account[] getAllAccounts() { return AccountManager.get(targetContext).getAccounts(); } - protected static void createDummyFiles() throws IOException { + protected static List createDummyFiles() throws IOException { File tempPath = new File(FileStorageUtils.getTemporalPath(account.name)); if (!tempPath.exists()) { assertTrue(tempPath.mkdirs()); @@ -242,9 +244,11 @@ protected static void createDummyFiles() throws IOException { assertTrue(tempPath.exists()); - createFile("empty.txt", 0); - createFile("nonEmpty.txt", 100); - createFile("chunkedFile.txt", 500000); + return Arrays.asList( + createFile("empty.txt", 0), + createFile("nonEmpty.txt", 100), + createFile("chunkedFile.txt", 500000) + ); } protected static File getDummyFile(String name) throws IOException { diff --git a/app/src/androidTest/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivityIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivityIT.kt index d915379aa970..a114c00dbdfa 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivityIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivityIT.kt @@ -1,6 +1,7 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2025 Philipp Hasper * SPDX-FileCopyrightText: 2025 Alper Ozturk * SPDX-FileCopyrightText: 2022 Tobias Kaminsky * SPDX-FileCopyrightText: 2022 Nextcloud GmbH @@ -8,23 +9,68 @@ */ package com.owncloud.android.ui.activity +import android.content.Intent +import android.net.Uri +import android.view.KeyEvent import androidx.test.core.app.launchActivity import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.hasDescendant import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isEnabled import androidx.test.espresso.matcher.ViewMatchers.isRoot +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import com.facebook.testing.screenshot.internal.TestNameDetector +import com.nextcloud.client.preferences.AppPreferencesImpl +import com.nextcloud.test.GrantStoragePermissionRule +import com.nextcloud.test.withSelectedText +import com.nextcloud.utils.extensions.removeFileExtension import com.owncloud.android.AbstractIT +import com.owncloud.android.R +import com.owncloud.android.datamodel.OCFile import com.owncloud.android.utils.ScreenshotTest +import org.hamcrest.Matchers.not +import org.junit.Before +import org.junit.Rule import org.junit.Test +import org.junit.rules.TestRule +import java.io.File class ReceiveExternalFilesActivityIT : AbstractIT() { - private val testClassName = "com.owncloud.android.ui.activity.ReceiveExternalFilesActivityIT" + + @get:Rule + var storagePermissionRule: TestRule = GrantStoragePermissionRule.grant() + + lateinit var mainFolder: OCFile + lateinit var subFolder: OCFile + lateinit var existingImageFile: OCFile + + @Before + fun setupFolderAndFileStructure() { + // Create folders with the necessary permissions and another test file + mainFolder = OCFile("/folder/").apply { + permissions = OCFile.PERMISSION_CAN_CREATE_FILE_AND_FOLDER + setFolder() + fileDataStorageManager.saveNewFile(this) + } + subFolder = OCFile("${mainFolder.remotePath}sub folder/").apply { + permissions = OCFile.PERMISSION_CAN_CREATE_FILE_AND_FOLDER + setFolder() + fileDataStorageManager.saveNewFile(this) + } + existingImageFile = OCFile("${mainFolder.remotePath}Existing Image File.jpg").apply { + fileDataStorageManager.saveNewFile(this) + } + } @Test @ScreenshotTest fun open() { + // Screenshot name must be constructed outside of the scenario, otherwise it will not be reliably detected + val screenShotName = TestNameDetector.getTestClass() + "_" + TestNameDetector.getTestName() launchActivity().use { scenario -> - val screenShotName = createName(testClassName + "_" + "open", "") onView(isRoot()).check(matches(isDisplayed())) scenario.onActivity { sut -> @@ -40,4 +86,161 @@ class ReceiveExternalFilesActivityIT : AbstractIT() { open() removeAccount(secondAccount) } + + fun createSendIntent(file: File): Intent = Intent(targetContext, ReceiveExternalFilesActivity::class.java).apply { + action = Intent.ACTION_SEND + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + putExtra(Intent.EXTRA_STREAM, Uri.fromFile(file)) + } + + fun createSendIntent(files: Iterable): Intent = + Intent(targetContext, ReceiveExternalFilesActivity::class.java).apply { + action = Intent.ACTION_SEND_MULTIPLE + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + putParcelableArrayListExtra(Intent.EXTRA_STREAM, ArrayList(files.map { Uri.fromFile(it) })) + } + + @Test + fun renameSingleFileUpload() { + val imageFile = getDummyFile("image.jpg") + val intent = createSendIntent(imageFile) + + // Store the folder in preferences, so the activity starts from there. + @Suppress("DEPRECATION") + val preferences = AppPreferencesImpl.fromContext(targetContext) + preferences.setLastUploadPath(mainFolder.remotePath) + + launchActivity(intent).use { + val expectedMainFolderTitle = (getCurrentActivity() as ToolbarActivity).getActionBarTitle(mainFolder, false) + // Verify that the test starts in the expected folder. If this fails, change the setup calls above + onView(withId(R.id.toolbar)) + .check(matches(hasDescendant(withText(expectedMainFolderTitle)))) + + onView(withText(R.string.uploader_btn_upload_text)) + .check(matches(isDisplayed())) + .check(matches(isEnabled())) + + // Test the pre-selection behavior (filename, but without extension, shall be selected) + onView(withId(R.id.user_input)) + .check(matches(withText(imageFile.name))) + .perform(ViewActions.click()) + .check(matches(withSelectedText(imageFile.name.removeFileExtension()))) + + // Set a new file name + val secondFileName = "New filename.jpg" + onView(withId(R.id.user_input)) + .perform(ViewActions.typeTextIntoFocusedView(secondFileName.removeFileExtension())) + .check(matches(withText(secondFileName))) + // Leave the field and come back to verify the pre-selection behavior correctly handles the new name + .perform(ViewActions.pressKey(KeyEvent.KEYCODE_TAB)) + .perform(ViewActions.click()) + .check(matches(withSelectedText(secondFileName.removeFileExtension()))) + onView(withText(R.string.uploader_btn_upload_text)) + .check(matches(isDisplayed())) + .check(matches(isEnabled())) + + // Set a file name without file extension + val thirdFileName = "No extension" + onView(withId(R.id.user_input)) + .perform(ViewActions.clearText()) + .perform(ViewActions.typeTextIntoFocusedView(thirdFileName)) + .check(matches(withText(thirdFileName))) + // Leave the field and come back to verify the pre-selection behavior correctly handles the new name + .perform(ViewActions.pressKey(KeyEvent.KEYCODE_TAB)) + .perform(ViewActions.click()) + .check(matches(withSelectedText(thirdFileName))) + onView(withText(R.string.uploader_btn_upload_text)) + .check(matches(isDisplayed())) + .check(matches(isEnabled())) + + // Test an invalid filename. Note: as the user is null, the capabilities are also null, so the name checker + // will not reject any special characters like '/'. So we only test empty and an existing file name + onView(withId(R.id.user_input)) + .perform(ViewActions.clearText()) + .check(matches(withText(""))) + onView(withText(R.string.uploader_btn_upload_text)) + .check(matches(isDisplayed())) + .check(matches(not(isEnabled()))) + onView(withId(R.id.user_input)) + .perform(ViewActions.click()) + .perform(ViewActions.typeTextIntoFocusedView(existingImageFile.fileName)) + .check(matches(withText(existingImageFile.fileName))) + onView(withText(R.string.uploader_btn_upload_text)) + .check(matches(isDisplayed())) + .check(matches(not(isEnabled()))) + + val fourthFileName = "New file name.jpg" + onView(withId(R.id.user_input)) + .perform(ViewActions.click()) + .perform(ViewActions.clearText()) + .perform(ViewActions.typeTextIntoFocusedView(fourthFileName)) + .check(matches(withText(fourthFileName))) + onView(withText(R.string.uploader_btn_upload_text)) + .check(matches(isDisplayed())) + .check(matches(isEnabled())) + + // Enter the subfolder and verify that the text stays intact + val expectedSubFolderTitle = (getCurrentActivity() as ToolbarActivity).getActionBarTitle(subFolder, false) + onView(withText(expectedSubFolderTitle)) + .perform(ViewActions.click()) + onView(withId(R.id.toolbar)) + .check(matches(hasDescendant(withText(expectedSubFolderTitle)))) + onView(withId(R.id.user_input)) + .check(matches(withText(fourthFileName))) + .perform(ViewActions.click()) + .check(matches(withSelectedText(fourthFileName.removeFileExtension()))) + + // Set a new, shorter file name + val fifthFileName = "short.jpg" + onView(withId(R.id.user_input)) + .perform(ViewActions.typeTextIntoFocusedView(fifthFileName.removeFileExtension())) + .check(matches(withText(fifthFileName))) + + // Start the upload, so the folder is stored in the preferences. + // Even though the upload is expected to fail because the backend is not mocked (yet?) + onView(withText(R.string.uploader_btn_upload_text)) + .check(matches(isDisplayed())) + .check(matches(isEnabled())) + .perform(ViewActions.click()) + } + + // Start a new file receive flow. Should now start in the sub folder, but with the original filename again + launchActivity(intent).use { + val expectedMainFolderTitle = (getCurrentActivity() as ToolbarActivity).getActionBarTitle(subFolder, false) + onView(withId(R.id.toolbar)) + .check(matches(hasDescendant(withText(expectedMainFolderTitle)))) + + onView(withText(R.string.uploader_btn_upload_text)) + .check(matches(isDisplayed())) + .check(matches(isEnabled())) + + onView(withId(R.id.user_input)) + .check(matches(withText(imageFile.name))) + } + } + + @Test + fun noRenameForMultiUpload() { + val testFiles = createDummyFiles() + val intent = createSendIntent(testFiles) + + // Store the folder in preferences, so the activity starts from there. + @Suppress("DEPRECATION") + val preferences = AppPreferencesImpl.fromContext(targetContext) + preferences.setLastUploadPath(mainFolder.remotePath) + + launchActivity(intent).use { + val expectedMainFolderTitle = (getCurrentActivity() as ToolbarActivity).getActionBarTitle(mainFolder, false) + // Verify that the test starts in the expected folder. If this fails, change the setup calls above + onView(withId(R.id.toolbar)) + .check(matches(hasDescendant(withText(expectedMainFolderTitle)))) + + onView(withText(R.string.uploader_btn_upload_text)) + .check(matches(isDisplayed())) + .check(matches(isEnabled())) + + onView(withId(R.id.user_input)) + .check(matches(not(isDisplayed()))) + } + } } diff --git a/app/src/main/java/com/owncloud/android/datamodel/OCFile.java b/app/src/main/java/com/owncloud/android/datamodel/OCFile.java index 03d85e22a902..7aaf0e05924f 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/OCFile.java +++ b/app/src/main/java/com/owncloud/android/datamodel/OCFile.java @@ -47,16 +47,16 @@ public class OCFile implements Parcelable, Comparable, ServerFileInterface { public final static String PERMISSION_CAN_RESHARE = "R"; - private final static String PERMISSION_SHARED = "S"; - private final static String PERMISSION_MOUNTED = "M"; - private final static String PERMISSION_CAN_CREATE_FILE_INSIDE_FOLDER = "C"; - private final static String PERMISSION_CAN_CREATE_FOLDER_INSIDE_FOLDER = "K"; - private final static String PERMISSION_CAN_READ = "G"; - private final static String PERMISSION_CAN_WRITE = "W"; - private final static String PERMISSION_CAN_DELETE_OR_LEAVE_SHARE = "D"; - private final static String PERMISSION_CAN_RENAME = "N"; - private final static String PERMISSION_CAN_MOVE = "V"; - private final static String PERMISSION_CAN_CREATE_FILE_AND_FOLDER = PERMISSION_CAN_CREATE_FILE_INSIDE_FOLDER + PERMISSION_CAN_CREATE_FOLDER_INSIDE_FOLDER; + public final static String PERMISSION_SHARED = "S"; + public final static String PERMISSION_MOUNTED = "M"; + public final static String PERMISSION_CAN_CREATE_FILE_INSIDE_FOLDER = "C"; + public final static String PERMISSION_CAN_CREATE_FOLDER_INSIDE_FOLDER = "K"; + public final static String PERMISSION_CAN_READ = "G"; + public final static String PERMISSION_CAN_WRITE = "W"; + public final static String PERMISSION_CAN_DELETE_OR_LEAVE_SHARE = "D"; + public final static String PERMISSION_CAN_RENAME = "N"; + public final static String PERMISSION_CAN_MOVE = "V"; + public final static String PERMISSION_CAN_CREATE_FILE_AND_FOLDER = PERMISSION_CAN_CREATE_FILE_INSIDE_FOLDER + PERMISSION_CAN_CREATE_FOLDER_INSIDE_FOLDER; private final static int MAX_FILE_SIZE_FOR_IMMEDIATE_PREVIEW_BYTES = 1024000; diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java index 2e8a6a27cdbf..da85c4de4b5e 100755 --- a/app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java @@ -1,6 +1,7 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2025 Philipp Hasper * SPDX-FileCopyrightText: 2023 TSI-mc * SPDX-FileCopyrightText: 2016-2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2019 Chris Narkiewicz @@ -24,11 +25,14 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.res.Resources.NotFoundException; +import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Parcelable; +import android.text.Editable; import android.text.TextUtils; +import android.text.TextWatcher; import android.text.format.DateFormat; import android.view.LayoutInflater; import android.view.Menu; @@ -87,6 +91,8 @@ import com.owncloud.android.utils.MimeType; import com.owncloud.android.utils.theme.ViewThemeUtils; +import org.apache.commons.io.FilenameUtils; + import java.io.File; import java.io.FileWriter; import java.io.IOException; @@ -96,6 +102,7 @@ import java.util.Arrays; import java.util.Calendar; import java.util.List; +import java.util.Objects; import java.util.Stack; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -110,6 +117,7 @@ import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog.Builder; import androidx.appcompat.widget.SearchView; +import androidx.core.util.Function; import androidx.core.view.MenuItemCompat; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.FragmentManager; @@ -117,6 +125,7 @@ import androidx.recyclerview.widget.LinearLayoutManager; import static com.owncloud.android.utils.DisplayUtils.openSortingOrderDialogFragment; +import static com.owncloud.android.utils.UriUtils.getDisplayNameForUri; /** * This can be used to upload things to an Nextcloud instance. @@ -124,7 +133,7 @@ public class ReceiveExternalFilesActivity extends FileActivity implements View.OnClickListener, CopyAndUploadContentUrisTask.OnCopyTmpFilesTaskListener, SortingOrderDialogFragment.OnSortingOrderListener, Injectable, AccountChooserInterface, - ReceiveExternalFilesAdapter.OnItemClickListener { + ReceiveExternalFilesAdapter.OnItemClickListener, TextWatcher { private static final String TAG = ReceiveExternalFilesActivity.class.getSimpleName(); @@ -141,10 +150,13 @@ public class ReceiveExternalFilesActivity extends FileActivity private AccountManager mAccountManager; private Stack mParents = new Stack<>(); - private List mStreamsToUpload; + @Nullable private List mStreamsToUpload; private String mUploadPath; private OCFile mFile; + @Nullable + private Function mFileDisplayNameTransformer = null; + private SyncBroadcastReceiver mSyncBroadcastReceiver; private ReceiveExternalFilesAdapter receiveExternalFilesAdapter; @@ -322,6 +334,53 @@ public void selectFile(OCFile file) { } } + @Override + public void afterTextChanged(Editable editable) {} + + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {} + + // TODO: this is copy-paste from RenameFileDialogFragment.kt + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + final var newFileName = Objects.requireNonNullElse(binding.userInput.getText(), "").toString(); + final var positiveButton = binding.uploaderChooseFolder; + + final var existingFiles = receiveExternalFilesAdapter.getFileNames(); + final var errorMessage = FileNameValidator.INSTANCE.checkFileName(newFileName, getCapabilities(), this, existingFiles); + + if (FileNameValidator.INSTANCE.isFileHidden(newFileName)) { + binding.userInputContainer.setError(getText(R.string.hidden_file_name_warning)); + positiveButton.setEnabled(true); + } else if (errorMessage != null) { + binding.userInputContainer.setError(errorMessage); + positiveButton.setEnabled(false); + } else if (checkExtensionRenamed(newFileName)) { + binding.userInputContainer.setError(getText(R.string.warn_rename_extension)); + positiveButton.setEnabled(true); + } else if (binding.userInputContainer.getError() != null) { + binding.userInputContainer.setError(null); + // Called to remove extra padding + binding.userInputContainer.setErrorEnabled(false); + positiveButton.setEnabled(true); + } + } + + private boolean checkExtensionRenamed(@NonNull String newFileName) { + if (mStreamsToUpload == null || mStreamsToUpload.size() != 1) { + return false; + } + final String previousFileName = getDisplayNameForUri((Uri) mStreamsToUpload.get(0), getActivity()); + if (previousFileName == null) { + return false; + } + + var previousExtension = FilenameUtils.getExtension(previousFileName); + var newExtension = FilenameUtils.getExtension(newFileName); + + return !Objects.equals(previousExtension, newExtension); + } + public static class DialogNoAccount extends DialogFragment { private final ViewThemeUtils viewThemeUtils; @@ -785,6 +844,7 @@ private void populateDirectoryList(OCFile file) { files = sortFileList(files); setupReceiveExternalFilesAdapter(files); } + setupFileNameInputField(); MaterialButton btnChooseFolder = binding.uploaderChooseFolder; viewThemeUtils.material.colorMaterialButtonPrimaryFilled(btnChooseFolder); @@ -838,6 +898,40 @@ public void setMessageForEmptyList(@StringRes final int headline, @StringRes fin }); } + private void setupFileNameInputField() { + binding.userInput.setVisibility(View.GONE); + mFileDisplayNameTransformer = null; + if (mStreamsToUpload == null || mStreamsToUpload.size() != 1) { + return; + } + final String fileName = getDisplayNameForUri((Uri) mStreamsToUpload.get(0), getActivity()); + if (fileName == null) { + return; + } + final String userProvidedFileName = Objects.requireNonNullElse(binding.userInput.getText(), "").toString(); + + binding.userInput.setVisibility(View.VISIBLE); + binding.userInput.setText(userProvidedFileName.isEmpty() ? fileName : userProvidedFileName); + binding.userInput.addTextChangedListener(this); + mFileDisplayNameTransformer = uri -> + Objects.requireNonNullElse(binding.userInput.getText(), fileName).toString(); + + // When entering the text field, pre-select the name (without extension if present), for convenient editing + binding.userInput.setOnFocusChangeListener((v, hasFocus) -> { + if (hasFocus) { + final String currentText = Objects.requireNonNullElse(binding.userInput.getText(), "").toString(); + binding.userInput.post(() -> { + if (currentText.lastIndexOf('.') != -1) { + binding.userInput.setSelection(0, currentText.lastIndexOf('.')); + } else { + // No file extension - select all + binding.userInput.selectAll(); + } + }); + } + }); + } + @Override public void onSavedCertificate() { startSyncFolderOperation(getCurrentDir()); @@ -961,7 +1055,8 @@ public void uploadFiles() { getUser().orElseThrow(RuntimeException::new), FileUploadWorker.LOCAL_BEHAVIOUR_DELETE, true, // Show waiting dialog while file is being copied from private storage - this // Copy temp task listener + this, // Listener for copying to temporary files + mFileDisplayNameTransformer ); UriUploader.UriUploaderResultCode resultCode = uploader.uploadUris(); diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/ReceiveExternalFilesAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/ReceiveExternalFilesAdapter.kt index 12736f80f651..a6b48b20c60e 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/ReceiveExternalFilesAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/ReceiveExternalFilesAdapter.kt @@ -26,6 +26,7 @@ import com.owncloud.android.datamodel.ThumbnailsCacheManager.ThumbnailGeneration import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.MimeTypeUtil import com.owncloud.android.utils.theme.ViewThemeUtils +import java.util.Objects @Suppress("LongParameterList") class ReceiveExternalFilesAdapter( @@ -159,4 +160,6 @@ class ReceiveExternalFilesAdapter( } override fun getItemCount() = filteredFiles.size + + fun getFileNames(): Set = files.map { it.fileName }.toSet() } diff --git a/app/src/main/java/com/owncloud/android/ui/helpers/UriUploader.kt b/app/src/main/java/com/owncloud/android/ui/helpers/UriUploader.kt index 9f08ad99bbce..6b4c63cd71d2 100644 --- a/app/src/main/java/com/owncloud/android/ui/helpers/UriUploader.kt +++ b/app/src/main/java/com/owncloud/android/ui/helpers/UriUploader.kt @@ -13,6 +13,7 @@ package com.owncloud.android.ui.helpers import android.content.ContentResolver import android.net.Uri import android.os.Parcelable +import androidx.core.util.Function import com.nextcloud.client.account.User import com.nextcloud.client.jobs.upload.FileUploadHelper import com.owncloud.android.R @@ -42,14 +43,16 @@ import com.owncloud.android.utils.UriUtils.getDisplayNameForUri "Detekt.SpreadOperator", "Detekt.TooGenericExceptionCaught" ) // legacy code -class UriUploader( +class UriUploader @JvmOverloads constructor( private val mActivity: FileActivity, private val mUrisToUpload: List, private val mUploadPath: String, private val user: User, private val mBehaviour: Int, private val mShowWaitingDialog: Boolean, - private val mCopyTmpTaskListener: OnCopyTmpFilesTaskListener? + private val mCopyTmpTaskListener: OnCopyTmpFilesTaskListener?, + /** If non-null, this function is called to determine the desired display name (i.e. filename) after upload**/ + private val mFileDisplayNameTransformer: Function? = null ) { enum class UriUploaderResultCode { @@ -113,7 +116,8 @@ class UriUploader( } private fun getRemotePathForUri(sourceUri: Uri): String { - val displayName = getDisplayNameForUri(sourceUri, mActivity) + val displayName = mFileDisplayNameTransformer?.apply(sourceUri) + ?: getDisplayNameForUri(sourceUri, mActivity) require(displayName != null) { "Display name cannot be null" } return mUploadPath + displayName } diff --git a/app/src/main/res/layout/receive_external_files.xml b/app/src/main/res/layout/receive_external_files.xml index 1f85eae78a32..b3806bd9478a 100644 --- a/app/src/main/res/layout/receive_external_files.xml +++ b/app/src/main/res/layout/receive_external_files.xml @@ -1,6 +1,7 @@