Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 67 additions & 28 deletions app/src/androidTest/java/com/owncloud/android/utils/FileUtilTest.kt
Original file line number Diff line number Diff line change
@@ -1,61 +1,100 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2020 Andy Scherzinger <info@andy-scherzinger.de>
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.owncloud.android.utils

import com.owncloud.android.AbstractIT
import org.junit.Assert
import androidx.test.platform.app.InstrumentationRegistry
import com.owncloud.android.utils.FileUtil.isFolderWritable
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import java.io.File

class FileUtilTest : AbstractIT() {
@Test
fun assertNullInput() {
Assert.assertEquals("", FileUtil.getFilenameFromPathString(null))

private lateinit var context: android.content.Context

@Before
fun setup() {
context = InstrumentationRegistry.getInstrumentation().targetContext
}

@Test
fun assertEmptyInput() {
Assert.assertEquals("", FileUtil.getFilenameFromPathString(""))
fun testIsFolderWritableWhenGivenCacheDirShouldReturnTrue() = runBlocking {
val writableDir = context.cacheDir
val result = isFolderWritable(writableDir)
assertTrue("Internal cache directory should be writable", result)
}

@Test
fun assertFileInput() {
val file = getDummyFile("empty.txt")
Assert.assertEquals("empty.txt", FileUtil.getFilenameFromPathString(file.absolutePath))
fun testIsFolderWritableWhenGivenNonExistentDirShouldReturnFalse() = runBlocking {
val nonExistentFile = File(context.cacheDir, "ghost_folder_123")
val result = isFolderWritable(nonExistentFile)
assertFalse("Non-existent folder should not be writable", result)
}

@Test
fun assertSlashInput() {
val tempPath = File(FileStorageUtils.getTemporalPath(account.name) + File.pathSeparator + "folder")
if (!tempPath.exists()) {
Assert.assertTrue(tempPath.mkdirs())
}
Assert.assertEquals("", FileUtil.getFilenameFromPathString(tempPath.absolutePath))
fun testIsFolderWritableWhenGivenFileShouldReturnFalse() = runBlocking {
val regularFile = File(context.cacheDir, "test_file.txt")
regularFile.createNewFile()
val result = isFolderWritable(regularFile)
assertFalse("A regular file should not be treated as a writable folder", result)
}

@Test
fun assertDotFileInput() {
val file = getDummyFile(".dotfile.ext")
Assert.assertEquals(".dotfile.ext", FileUtil.getFilenameFromPathString(file.absolutePath))
fun testIsFolderWritableWhenGivenNullShouldReturnFalse() = runBlocking {
val result = isFolderWritable(null)
assertFalse("Null input should return false", result)
}

@Test
fun assertFolderInput() {
val tempPath = File(FileStorageUtils.getTemporalPath(account.name))
if (!tempPath.exists()) {
Assert.assertTrue(tempPath.mkdirs())
fun testIsFolderWritableWhenGivenReadOnlyDirShouldReturnFalse() = runBlocking {
val readOnlyDir = File(context.cacheDir, "readonly_test")
readOnlyDir.mkdir()

try {
readOnlyDir.setReadOnly()
val result = isFolderWritable(readOnlyDir)
assertFalse("Read-only directory should return false", result)
} finally {
readOnlyDir.setWritable(true)
readOnlyDir.delete()
}
}

Assert.assertEquals("", FileUtil.getFilenameFromPathString(tempPath.absolutePath))
@Test
fun testIsFolderWritableWhenGivenNestedStructureShouldReturnTrue() = runBlocking {
val rootDir = File(context.cacheDir, "test_root")
rootDir.mkdir()

try {
val result = isFolderWritable(rootDir)
assertTrue("Should be able to create and delete nested temp structures", result)
val children = rootDir.list()
assertTrue("Temp directory should have been cleaned up", children == null || children.isEmpty())
} finally {
rootDir.delete()
}
}

@Test
fun assertNoFileExtensionInput() {
val file = getDummyFile("file")
Assert.assertEquals("file", FileUtil.getFilenameFromPathString(file.absolutePath))
fun testIsFolderWritableWhenGivenReadonlyNestedStructureShouldReturnFalse() = runBlocking {
val readOnlyDir = File(context.cacheDir, "locked_dir")
readOnlyDir.mkdir()
readOnlyDir.setReadOnly()

try {
val result = isFolderWritable(readOnlyDir)
assertFalse("Should return false if temp folder creation fails", result)
} finally {
readOnlyDir.setWritable(true)
readOnlyDir.delete()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
import com.owncloud.android.ui.fragment.ExtendedListFragment;
import com.owncloud.android.ui.fragment.LocalFileListFragment;
import com.owncloud.android.utils.FileSortOrder;
import com.owncloud.android.utils.FileStorageUtils;
import com.owncloud.android.utils.FileUtil;
import com.owncloud.android.utils.PermissionUtil;

import java.io.File;
Expand All @@ -69,6 +69,8 @@
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import kotlin.Unit;
import kotlin.jvm.functions.Function1;

import static com.owncloud.android.ui.activity.FileActivity.EXTRA_USER;

Expand Down Expand Up @@ -596,23 +598,24 @@ public void onDirectoryClick(File directory) {
}

private void checkWritableFolder(File folder) {
boolean canWriteIntoFolder = FileStorageUtils.isFolderWritable(folder);

binding.uploadFilesSpinnerBehaviour.setEnabled(canWriteIntoFolder);
FileUtil.INSTANCE.isFolderWritable(folder, getLifecycle(), canWriteIntoFolder -> {
binding.uploadFilesSpinnerBehaviour.setEnabled(canWriteIntoFolder);

TextView textView = findViewById(R.id.upload_files_upload_files_behaviour_text);
TextView textView = findViewById(R.id.upload_files_upload_files_behaviour_text);

if (canWriteIntoFolder) {
textView.setText(getString(R.string.uploader_upload_files_behaviour));
int localBehaviour = preferences.getUploaderBehaviour();
binding.uploadFilesSpinnerBehaviour.setSelection(localBehaviour);
} else {
binding.uploadFilesSpinnerBehaviour.setSelection(1);
textView.setText(new StringBuilder().append(getString(R.string.uploader_upload_files_behaviour))
.append(' ')
.append(getString(R.string.uploader_upload_files_behaviour_not_writable))
.toString());
}
if (canWriteIntoFolder) {
textView.setText(getString(R.string.uploader_upload_files_behaviour));
int localBehaviour = preferences.getUploaderBehaviour();
binding.uploadFilesSpinnerBehaviour.setSelection(localBehaviour);
} else {
binding.uploadFilesSpinnerBehaviour.setSelection(1);
textView.setText(new StringBuilder().append(getString(R.string.uploader_upload_files_behaviour))
.append(' ')
.append(getString(R.string.uploader_upload_files_behaviour_not_writable))
.toString());
}
return Unit.INSTANCE;
});
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import android.view.View
import android.widget.AdapterView
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.nextcloud.client.di.Injectable
import com.nextcloud.client.preferences.SubFolderRule
Expand All @@ -35,7 +36,11 @@ import com.owncloud.android.ui.activity.UploadFilesActivity
import com.owncloud.android.ui.dialog.parcel.SyncedFolderParcelable
import com.owncloud.android.utils.DisplayUtils
import com.owncloud.android.utils.FileStorageUtils
import com.owncloud.android.utils.FileUtil
import com.owncloud.android.utils.theme.ViewThemeUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import javax.inject.Inject

Expand Down Expand Up @@ -279,20 +284,29 @@ class SyncedFolderPreferencesDialogFragment :
binding?.settingInstantBehaviourContainer?.alpha = ALPHA_DISABLED
return
}
if (syncedFolder!!.localPath != null &&
FileStorageUtils.isFolderWritable(File(syncedFolder!!.localPath))
) {
binding?.settingInstantBehaviourContainer?.isEnabled = true
binding?.settingInstantBehaviourContainer?.alpha = ALPHA_ENABLED
binding?.settingInstantBehaviourSummary?.text =
uploadBehaviorItemStrings[syncedFolder!!.uploadActionInteger]
} else {
binding?.settingInstantBehaviourContainer?.isEnabled = false
binding?.settingInstantBehaviourContainer?.alpha = ALPHA_DISABLED
syncedFolder?.setUploadAction(
resources.getTextArray(R.array.pref_behaviour_entryValues)[0].toString()
)
binding?.settingInstantBehaviourSummary?.setText(R.string.auto_upload_file_behaviour_kept_in_folder)

val folderFile = syncedFolder?.localPath?.let { File(it) }
lifecycleScope.launch {
val writable = FileUtil.isFolderWritable(folderFile)
withContext(Dispatchers.Main) {
binding?.settingInstantBehaviourContainer?.run {
if (writable) {
isEnabled = true
alpha = ALPHA_ENABLED
binding?.settingInstantBehaviourSummary?.text =
uploadBehaviorItemStrings[syncedFolder!!.uploadActionInteger]
} else {
isEnabled = false
alpha = ALPHA_DISABLED
syncedFolder?.setUploadAction(
resources.getTextArray(R.array.pref_behaviour_entryValues)[0].toString()
)
binding?.settingInstantBehaviourSummary?.setText(
R.string.auto_upload_file_behaviour_kept_in_folder
)
}
}
}
}
}

Expand Down
10 changes: 0 additions & 10 deletions app/src/main/java/com/owncloud/android/utils/FileStorageUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -791,16 +791,6 @@ public static boolean checkIfEnoughSpace(OCFile file) {
return checkIfEnoughSpace(availableSpaceOnDevice, file);
}

public static boolean isFolderWritable(File folder) {
File[] children = folder.listFiles();

if (children != null && children.length > 0) {
return children[0].canWrite();
} else {
return folder.canWrite();
}
}

@VisibleForTesting
public static boolean checkIfEnoughSpace(long availableSpaceOnDevice, OCFile file) {
if (file.isFolder()) {
Expand Down
61 changes: 0 additions & 61 deletions app/src/main/java/com/owncloud/android/utils/FileUtil.java

This file was deleted.

68 changes: 68 additions & 0 deletions app/src/main/java/com/owncloud/android/utils/FileUtil.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.owncloud.android.utils

import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope
import com.owncloud.android.lib.common.utils.Log_OC
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.io.IOException
import java.nio.file.Files
import java.nio.file.attribute.BasicFileAttributes
import java.util.concurrent.TimeUnit

object FileUtil {
private const val TAG = "FileUtil"

@JvmStatic
fun getCreationTimestamp(file: File): Long? {
try {
return Files.readAttributes(file.toPath(), BasicFileAttributes::class.java)
.creationTime()
.to(TimeUnit.SECONDS)
} catch (_: IOException) {
Log_OC.e(
TAG,
"failed to read creation timestamp for file: " + file.getName()
)
return null
}
}

fun isFolderWritable(folder: File?, lifecycle: Lifecycle, onCompleted: (Boolean) -> Unit) {
lifecycle.coroutineScope.launch {
val result = isFolderWritable(folder)
withContext(Dispatchers.Main) {
onCompleted(result)
}
}
}

suspend fun isFolderWritable(folder: File?): Boolean = withContext(Dispatchers.IO) {
if (folder == null || !folder.isDirectory() || !folder.canWrite()) {
return@withContext false
}

return@withContext try {
val tempDir = File(folder, ".test_write_dir_${System.currentTimeMillis()}")
if (!tempDir.mkdir()) return@withContext false

val tempFile = File(tempDir, "test_file")
val created = tempFile.createNewFile()

if (created) tempFile.delete()
tempDir.delete()

created
} catch (_: Exception) {
false
}
}
}
Loading