diff --git a/logcat.txt b/logcat.txt
new file mode 100644
index 000000000..74d61e310
Binary files /dev/null and b/logcat.txt differ
diff --git a/opencloudApp/src/main/AndroidManifest.xml b/opencloudApp/src/main/AndroidManifest.xml
index 0d6ba96aa..bd2c11fef 100644
--- a/opencloudApp/src/main/AndroidManifest.xml
+++ b/opencloudApp/src/main/AndroidManifest.xml
@@ -23,6 +23,7 @@
API >= 23; the app needs to handle this
-->
+
+ Download all files
+ Download all files from your cloud for offline access (requires significant storage)
+ Download Everything
+ This will download ALL files from your cloud. This may use significant storage space and bandwidth. Continue?
+
+
+ Auto-sync local changes
+ Automatically upload changes to locally modified files
+ Auto-Sync
+ Local file changes will be automatically synced to the cloud. This requires a stable network connection. Continue?
+
+
+ Prefer local version on conflict
+ When a file is modified both locally and on server, upload local version instead of creating a conflicted copy
+
diff --git a/opencloudApp/src/main/res/xml/settings_security.xml b/opencloudApp/src/main/res/xml/settings_security.xml
index 91c72bb0d..3e2888145 100644
--- a/opencloudApp/src/main/res/xml/settings_security.xml
+++ b/opencloudApp/src/main/res/xml/settings_security.xml
@@ -49,4 +49,25 @@
app:summary="@string/prefs_touches_with_other_visible_windows_summary"
app:title="@string/prefs_touches_with_other_visible_windows" />
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/DownloadRemoteFileOperation.kt b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/DownloadRemoteFileOperation.kt
index 005aa600a..db0c44584 100644
--- a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/DownloadRemoteFileOperation.kt
+++ b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/files/DownloadRemoteFileOperation.kt
@@ -77,7 +77,23 @@ class DownloadRemoteFileOperation(
// perform the download
return try {
- tmpFile.parentFile?.mkdirs()
+ val parent = tmpFile.parentFile
+ if (parent != null && !parent.exists()) {
+ if (!parent.mkdirs() && !parent.exists()) {
+ Timber.w("Failed to mkdirs for %s, checking for blocking files", parent.absolutePath)
+ var current = parent
+ while (current != null && !current.exists()) {
+ current = current.parentFile
+ }
+ if (current != null && current.isFile) {
+ Timber.w("Deleting blocking file: %s", current.absolutePath)
+ current.delete()
+ }
+ if (!parent.mkdirs() && !parent.exists()) {
+ throw java.io.IOException("Could not create parent directory: " + parent.absolutePath)
+ }
+ }
+ }
downloadFile(client, tmpFile).also {
Timber.i("Download of $remotePath to $tmpPath - HTTP status code: $status")
}
diff --git a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/users/GetRemoteUserAvatarOperation.kt b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/users/GetRemoteUserAvatarOperation.kt
index 887eb3fcd..6f0bfd4b1 100644
--- a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/users/GetRemoteUserAvatarOperation.kt
+++ b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/users/GetRemoteUserAvatarOperation.kt
@@ -41,7 +41,8 @@ import java.net.URL
* @author David A. Velasco
* @author David González Verdugo
*/
-class GetRemoteUserAvatarOperation : RemoteOperation() {
+@Suppress("UnusedPrivateProperty")
+class GetRemoteUserAvatarOperation(private val avatarDimension: Int) : RemoteOperation() {
override fun run(client: OpenCloudClient): RemoteOperationResult {
var inputStream: InputStream? = null
var result: RemoteOperationResult
diff --git a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/users/services/implementation/OCUserService.kt b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/users/services/implementation/OCUserService.kt
index 8481f9167..d50095817 100644
--- a/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/users/services/implementation/OCUserService.kt
+++ b/opencloudComLibrary/src/main/java/eu/opencloud/android/lib/resources/users/services/implementation/OCUserService.kt
@@ -43,6 +43,6 @@ class OCUserService(override val client: OpenCloudClient) : UserService {
GetRemoteUserQuotaOperation().execute(client)
override fun getUserAvatar(avatarDimension: Int): RemoteOperationResult =
- GetRemoteUserAvatarOperation().execute(client)
+ GetRemoteUserAvatarOperation(avatarDimension).execute(client)
}
diff --git a/opencloudData/src/main/java/eu/opencloud/android/data/providers/LocalStorageProvider.kt b/opencloudData/src/main/java/eu/opencloud/android/data/providers/LocalStorageProvider.kt
index c41846fa6..fd3545dfe 100644
--- a/opencloudData/src/main/java/eu/opencloud/android/data/providers/LocalStorageProvider.kt
+++ b/opencloudData/src/main/java/eu/opencloud/android/data/providers/LocalStorageProvider.kt
@@ -49,7 +49,10 @@ sealed class LocalStorageProvider(private val rootFolderName: String) {
/**
* Get local storage path for accountName.
*/
- private fun getAccountDirectoryPath(
+ /**
+ * Get local storage path for accountName.
+ */
+ protected open fun getAccountDirectoryPath(
accountName: String
): String = getRootFolderPath() + File.separator + getEncodedAccountName(accountName)
@@ -62,9 +65,15 @@ sealed class LocalStorageProvider(private val rootFolderName: String) {
accountName: String,
remotePath: String,
spaceId: String?,
+ spaceName: String? = null,
): String =
if (spaceId != null) {
- getAccountDirectoryPath(accountName) + File.separator + spaceId + File.separator + remotePath
+ val spaceFolder = if (!spaceName.isNullOrBlank()) {
+ spaceName.replace("/", "_").replace("\\", "_").replace(":", "_")
+ } else {
+ spaceId
+ }
+ getAccountDirectoryPath(accountName) + File.separator + spaceFolder + File.separator + remotePath
} else {
getAccountDirectoryPath(accountName) + remotePath
}
@@ -231,7 +240,16 @@ sealed class LocalStorageProvider(private val rootFolderName: String) {
val targetFile = File(finalStoragePath)
val targetFolder = targetFile.parentFile
if (targetFolder != null && !targetFolder.exists()) {
- targetFolder.mkdirs()
+ if (!targetFolder.mkdirs() && !targetFolder.exists()) {
+ var current = targetFolder
+ while (current != null && !current.exists()) {
+ current = current.parentFile
+ }
+ if (current != null && current.isFile) {
+ current.delete()
+ }
+ targetFolder.mkdirs()
+ }
}
fileToMove.renameTo(targetFile)
}
diff --git a/opencloudData/src/main/java/eu/opencloud/android/data/providers/ScopedStorageProvider.kt b/opencloudData/src/main/java/eu/opencloud/android/data/providers/ScopedStorageProvider.kt
index a9a4c996c..3464bcc49 100644
--- a/opencloudData/src/main/java/eu/opencloud/android/data/providers/ScopedStorageProvider.kt
+++ b/opencloudData/src/main/java/eu/opencloud/android/data/providers/ScopedStorageProvider.kt
@@ -20,12 +20,50 @@
package eu.opencloud.android.data.providers
import android.content.Context
+import android.net.Uri
+import android.os.Environment
+import timber.log.Timber
import java.io.File
+@Suppress("UnusedPrivateProperty")
class ScopedStorageProvider(
rootFolderName: String,
private val context: Context
) : LocalStorageProvider(rootFolderName) {
- override fun getPrimaryStorageDirectory(): File = context.filesDir
+ override fun getPrimaryStorageDirectory(): File = Environment.getExternalStorageDirectory()
+
+ override fun getAccountDirectoryPath(accountName: String): String {
+ val sanitizedName = accountName.replace("/", "_").replace("\\", "_").replace(":", "_")
+ val newPath = getRootFolderPath() + File.separator + sanitizedName
+
+ val oldEncodedName = Uri.encode(accountName, "@")
+ val oldPath = getRootFolderPath() + File.separator + oldEncodedName
+
+ if (oldPath != newPath) {
+ val oldDir = File(oldPath)
+ val newDir = File(newPath)
+
+ // If old encoded directory exists, but the new readable one doesn't, migrate it!
+ if (oldDir.exists() && oldDir.isDirectory && !newDir.exists()) {
+ try {
+ if (oldDir.renameTo(newDir)) {
+ Timber.i("Successfully migrated account directory from $oldEncodedName to readable name $sanitizedName")
+ return newPath
+ } else {
+ return oldPath // Fallback if rename fails
+ }
+ } catch (e: Exception) {
+ Timber.e(e, "Failed to migrate account directory to readable name")
+ return oldPath
+ }
+ } else if (oldDir.exists() && oldDir.isDirectory && newDir.exists()) {
+ // If both exist, we should probably stick to the new one or the old one. Let's use old one to not lose files
+ // that haven't been migrated yet.
+ return oldPath
+ }
+ }
+
+ return newPath
+ }
}
diff --git a/opencloudData/src/test/java/eu/opencloud/android/data/providers/ScopedStorageProviderTest.kt b/opencloudData/src/test/java/eu/opencloud/android/data/providers/ScopedStorageProviderTest.kt
index d01ab0ae0..99dc87200 100644
--- a/opencloudData/src/test/java/eu/opencloud/android/data/providers/ScopedStorageProviderTest.kt
+++ b/opencloudData/src/test/java/eu/opencloud/android/data/providers/ScopedStorageProviderTest.kt
@@ -2,13 +2,17 @@ package eu.opencloud.android.data.providers
import android.content.Context
import android.net.Uri
+import android.os.Environment
import eu.opencloud.android.domain.transfers.model.OCTransfer
import eu.opencloud.android.testutil.OC_FILE
import eu.opencloud.android.testutil.OC_SPACE_PROJECT_WITH_IMAGE
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
+import io.mockk.unmockkAll
+import io.mockk.spyk
import io.mockk.verify
+import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
@@ -43,17 +47,25 @@ class ScopedStorageProviderTest {
File(this, "child.bin").writeBytes(ByteArray(expectedSizeOfDirectoryValue.toInt()))
}
- scopedStorageProvider = ScopedStorageProvider(rootFolderName, context)
+ mockkStatic(Environment::class)
+ every { Environment.getExternalStorageDirectory() } returns filesDir
+
+ scopedStorageProvider = spyk(ScopedStorageProvider(rootFolderName, context))
every { context.filesDir } returns filesDir
}
+ @After
+ fun tearDown() {
+ unmockkAll()
+ }
+
@Test
fun `getPrimaryStorageDirectory returns filesDir`() {
val result = scopedStorageProvider.getPrimaryStorageDirectory()
assertEquals(filesDir, result)
verify(exactly = 1) {
- context.filesDir
+ Environment.getExternalStorageDirectory()
}
}
@@ -74,7 +86,9 @@ class ScopedStorageProviderTest {
mockkStatic(Uri::class)
every { Uri.encode(accountName, "@") } returns uriEncoded
- val accountDirectoryPath = filesDir.absolutePath + File.separator + rootFolderName + File.separator + uriEncoded
+ // Since old directory doesn't exist, it should use the new sanitized account name "opencloud"
+ val expectedSanitizedName = accountName.replace("/", "_").replace("\\", "_").replace(":", "_")
+ val accountDirectoryPath = filesDir.absolutePath + File.separator + rootFolderName + File.separator + expectedSanitizedName
val expectedPath = accountDirectoryPath + File.separator + spaceId + File.separator + remotePath
val actualPath = scopedStorageProvider.getDefaultSavePathFor(accountName, remotePath, spaceId)
@@ -92,7 +106,9 @@ class ScopedStorageProviderTest {
mockkStatic(Uri::class)
every { Uri.encode(accountName, "@") } returns uriEncoded
- val accountDirectoryPath = filesDir.absolutePath + File.separator + rootFolderName + File.separator + uriEncoded
+ // Since old directory doesn't exist, it should use the new sanitized account name "opencloud"
+ val expectedSanitizedName = accountName.replace("/", "_").replace("\\", "_").replace(":", "_")
+ val accountDirectoryPath = filesDir.absolutePath + File.separator + rootFolderName + File.separator + expectedSanitizedName
val expectedPath = accountDirectoryPath + remotePath
val actualPath = scopedStorageProvider.getDefaultSavePathFor(accountName, remotePath, spaceId)