Skip to content

Commit 86b4acc

Browse files
committed
manual backport
Signed-off-by: alperozturk96 <alper_ozturk@proton.me>
1 parent 009514e commit 86b4acc

27 files changed

Lines changed: 1791 additions & 165 deletions

app/schemas/com.nextcloud.client.database.NextcloudDatabase/106.json

Lines changed: 1388 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
package com.nextcloud.test
8+
9+
import org.junit.runner.Description
10+
import org.junit.runner.manipulation.Filter
11+
12+
class FlakyTestFilter : Filter() {
13+
override fun shouldRun(description: Description): Boolean = when {
14+
description.isTest -> !description.isFlaky()
15+
else -> description.children.any { shouldRun(it) }
16+
}
17+
18+
override fun describe(): String = "skip tests annotated with @Flaky"
19+
20+
private fun Description.isFlaky(): Boolean {
21+
val onMethod = getAnnotation(Flaky::class.java) != null
22+
val onClass = testClass?.isAnnotationPresent(Flaky::class.java) == true
23+
return onMethod || onClass
24+
}
25+
}

app/src/androidTest/java/com/owncloud/android/GrantFolderExistenceTests.kt

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import org.junit.Assert.assertEquals
2020
import org.junit.Assert.assertFalse
2121
import org.junit.Assert.assertNotNull
2222
import org.junit.Assert.assertTrue
23+
import org.junit.Assert.fail
2324
import org.junit.Before
2425
import org.junit.Test
2526
import java.io.IOException
@@ -50,7 +51,7 @@ class GrantFolderExistenceTests : AbstractOnServerIT() {
5051
assertTrue("month folder should exist on server", existsOnServer(monthFolder))
5152
assertNotNull("month folder should be cached locally", storageManager.getFileByDecryptedRemotePath(monthFolder))
5253

53-
removeYearFolderOnServerOnly()
54+
removeOnServer(yearFolder)
5455

5556
assertFalse("month folder should be deleted", existsOnServer(monthFolder))
5657
assertNotNull(
@@ -74,10 +75,7 @@ class GrantFolderExistenceTests : AbstractOnServerIT() {
7475
fun testUploadFileThenDeleteRootOnServerOnlyThenUploadAgainShouldRecreateAllFolderLevelsAndReturnOk() {
7576
uploadAndAssertSuccess("first.txt")
7677

77-
assertTrue(
78-
"root folder should be removed",
79-
RemoveFileRemoteOperation(root).execute(client).isSuccess
80-
)
78+
removeOnServer(root)
8179
assertFalse(existsOnServer(root))
8280

8381
val result = upload("nonEmpty.txt", monthFolder + "nonEmpty.txt")
@@ -98,11 +96,18 @@ class GrantFolderExistenceTests : AbstractOnServerIT() {
9896
assertTrue("uploaded file should exist on server", existsOnServer(monthFolder + filename))
9997
}
10098

101-
private fun removeYearFolderOnServerOnly() {
102-
assertTrue(
103-
"year folder should be removed",
104-
RemoveFileRemoteOperation(yearFolder).execute(client).isSuccess
105-
)
99+
private fun removeOnServer(remotePath: String) {
100+
// the server keeps a transient lock on a just uploaded file, so a DELETE on one of its
101+
// parent folders answers 423 until that lock expires
102+
repeat(REMOVE_ATTEMPTS) {
103+
if (RemoveFileRemoteOperation(remotePath).execute(client).isSuccess) {
104+
return
105+
}
106+
107+
shortSleep()
108+
}
109+
110+
fail("$remotePath should be removed on server")
106111
}
107112

108113
private fun existsOnServer(remotePath: String): Boolean =
@@ -139,5 +144,6 @@ class GrantFolderExistenceTests : AbstractOnServerIT() {
139144

140145
companion object {
141146
private const val FILE_LINE_COUNT = 100
147+
private const val REMOVE_ATTEMPTS = 5
142148
}
143149
}

app/src/androidTest/java/com/owncloud/android/providers/DocumentsStorageProviderIT.kt

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,15 @@ package com.owncloud.android.providers
88

99
import android.provider.DocumentsContract
1010
import androidx.documentfile.provider.DocumentFile
11+
import com.nextcloud.client.account.UserAccountManagerImpl
12+
import com.nextcloud.client.jobs.upload.FileUploadHelper
1113
import com.nextcloud.test.RandomStringGenerator
1214
import com.owncloud.android.AbstractOnServerIT
1315
import com.owncloud.android.R
1416
import com.owncloud.android.datamodel.OCFile.ROOT_PATH
17+
import com.owncloud.android.datamodel.UploadsStorageManager
18+
import com.owncloud.android.datamodel.UploadsStorageManager.UploadStatus
19+
import com.owncloud.android.db.OCUpload
1520
import com.owncloud.android.lib.common.utils.Log_OC
1621
import com.owncloud.android.providers.DocumentsProviderUtils.assertExistsOnServer
1722
import com.owncloud.android.providers.DocumentsProviderUtils.assertListFilesEquals
@@ -31,12 +36,16 @@ import org.junit.After
3136
import org.junit.Assert.assertEquals
3237
import org.junit.Assert.assertFalse
3338
import org.junit.Assert.assertTrue
39+
import org.junit.Assert.fail
3440
import org.junit.Before
3541
import org.junit.Test
3642
import kotlin.random.Random
3743

3844
private const val MAX_FILE_NAME_LENGTH = 225
3945

46+
// the provider can only enqueue the upload worker, so give it a generous budget before giving up
47+
private const val UPLOAD_POLL_ATTEMPTS = 45
48+
4049
class DocumentsStorageProviderIT : AbstractOnServerIT() {
4150

4251
private val context = targetContext
@@ -48,8 +57,17 @@ class DocumentsStorageProviderIT : AbstractOnServerIT() {
4857
private val uri = DocumentsContract.buildTreeDocumentUri(authority, documentId)
4958
private val rootDir get() = DocumentFile.fromTreeUri(context, uri)!!
5059

60+
private val uploadsStorageManager = UploadsStorageManager(
61+
UserAccountManagerImpl.fromContext(context),
62+
contentResolver
63+
)
64+
5165
@Before
5266
fun before() {
67+
// an upload left over by a previous test fails once its local file is gone, and WorkManager
68+
// then drops everything appended behind it in the upload chain, including our own uploads
69+
FileUploadHelper.instance().cancel(user.accountName)
70+
5371
// DocumentsProvider#onCreate() is called when the application is started
5472
// which is *after* AbstractOnServerIT adds the accounts (when the app is freshly installed).
5573
// So we need to query our roots here to ensure that the internal storage map is initialized.
@@ -211,11 +229,7 @@ class DocumentsStorageProviderIT : AbstractOnServerIT() {
211229
it!!.write(content1)
212230
}
213231

214-
// refresh
215-
while (file1.getOCFile(storageManager)!!.etagOnServer == createdETag) {
216-
shortSleep()
217-
rootDir.listFiles()
218-
}
232+
awaitUploadedToServer(file1, createdETag)
219233

220234
val remotePath = file1.getOCFile(storageManager)!!.remotePath
221235

@@ -250,11 +264,7 @@ class DocumentsStorageProviderIT : AbstractOnServerIT() {
250264
it!!.write(content1)
251265
}
252266

253-
// refresh
254-
while (file1.getOCFile(storageManager)!!.etagOnServer == createdETag) {
255-
shortSleep()
256-
rootDir.listFiles()
257-
}
267+
awaitUploadedToServer(file1, createdETag)
258268

259269
val content2 = "new content".toByteArray()
260270

@@ -266,4 +276,31 @@ class DocumentsStorageProviderIT : AbstractOnServerIT() {
266276
val bytes = contentResolver.openInputStream(file1.uri)?.readBytes() ?: ByteArray(0)
267277
assertEquals(String(content2), String(bytes))
268278
}
279+
280+
private fun awaitUploadedToServer(file: DocumentFile, etagBeforeUpload: String) {
281+
val remotePath = file.getOCFile(storageManager)!!.remotePath
282+
283+
repeat(UPLOAD_POLL_ATTEMPTS) {
284+
if (file.getOCFile(storageManager)!!.etagOnServer != etagBeforeUpload) {
285+
return
286+
}
287+
288+
failedUpload(remotePath)?.let {
289+
fail("upload of $remotePath failed with ${it.lastResult}")
290+
}
291+
292+
shortSleep()
293+
rootDir.listFiles()
294+
}
295+
296+
fail("upload of $remotePath did not finish, stored uploads: ${describeUploads()}")
297+
}
298+
299+
private fun failedUpload(remotePath: String): OCUpload? = uploadsStorageManager
300+
.getUploadsForAccount(user.accountName)
301+
.firstOrNull { it.remotePath == remotePath && it.uploadStatus == UploadStatus.UPLOAD_FAILED }
302+
303+
private fun describeUploads(): String = uploadsStorageManager
304+
.getUploadsForAccount(user.accountName)
305+
.joinToString { "${it.remotePath} is ${it.uploadStatus} with ${it.lastResult}" }
269306
}

app/src/androidTest/java/com/owncloud/android/ui/fragment/UnifiedSearchFragmentIT.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ class UnifiedSearchFragmentIT : AbstractIT() {
6767
scenario.onActivity { activity ->
6868
val sut = UnifiedSearchFragment.newInstance(null, null, "/")
6969
val testViewModel = UnifiedSearchViewModel(activity.application)
70+
testViewModel.setCurrentAccountProvider(activity.userAccountManager)
7071
testViewModel.setConnectivityService(activity.connectivityServiceMock)
7172
val localRepository = UnifiedSearchFakeRepository()
7273
testViewModel.setRepository(localRepository)

app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ import com.owncloud.android.db.ProviderMeta
104104
AutoMigration(from = 101, to = 102, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class),
105105
AutoMigration(from = 102, to = 103, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class),
106106
AutoMigration(from = 103, to = 104),
107-
AutoMigration(from = 104, to = 105)
107+
AutoMigration(from = 104, to = 105),
108+
AutoMigration(from = 105, to = 106, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class)
108109
],
109110
exportSchema = true
110111
)

app/src/main/java/com/nextcloud/client/database/entity/CapabilityEntity.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,9 @@ data class CapabilityEntity(
157157
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_MOD_REWRITE_WORKING)
158158
val modRewriteWorking: Int?,
159159
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_CHUNKED_UPLOAD_MAX_SIZE)
160-
val chunkedUploadMaxSize: Long?
160+
val chunkedUploadMaxSize: Long?,
161+
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SHARING_JSON)
162+
val sharingJson: String?
161163
)
162164

163165
@Suppress("LongMethod", "ReturnCount")

app/src/main/java/com/nextcloud/client/player/ui/PlayerLauncher.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import com.nextcloud.client.player.model.file.PlaybackFiles
1919
import com.nextcloud.client.player.model.file.PlaybackFilesComparator
2020
import com.nextcloud.client.player.model.file.PlaybackFilesRepository
2121
import com.nextcloud.client.player.util.PlayerUtil.toPlaybackFile
22+
import com.nextcloud.utils.extensions.resolveMimeType
2223
import com.owncloud.android.datamodel.OCFile
2324
import kotlinx.coroutines.Job
2425
import kotlinx.coroutines.launch
@@ -68,7 +69,7 @@ class PlayerLauncher @Inject constructor(
6869
}
6970

7071
private suspend fun prepareQueue(file: OCFile, collection: PlaybackCollection): PlaybackFileType {
71-
val fileType = PlaybackFileType.ofMimeType(file.mimeType)
72+
val fileType = PlaybackFileType.ofMimeType(file.resolveMimeType())
7273
playbackResumptionConfigStore.saveConfig(file.localId.toString(), file.parentId, fileType, collection)
7374

7475
playbackModel.start()

app/src/main/java/com/nextcloud/client/player/util/PlayerUtil.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import com.nextcloud.client.player.model.state.PlaybackState
3434
import com.nextcloud.client.player.model.state.PlayerState
3535
import com.nextcloud.client.player.model.state.RepeatMode
3636
import com.nextcloud.client.player.model.state.VideoSize
37+
import com.nextcloud.utils.extensions.resolveMimeType
3738
import com.owncloud.android.datamodel.OCFile
3839
import com.owncloud.android.lib.resources.shares.OCShare
3940
import com.owncloud.android.utils.MimeTypeUtil
@@ -177,7 +178,7 @@ object PlayerUtil {
177178
id = localId.toString(),
178179
uri = getPlaybackUri().toString(),
179180
name = fileName,
180-
mimeType = mimeType,
181+
mimeType = resolveMimeType(),
181182
contentLength = fileLength,
182183
lastModified = modificationTimestamp,
183184
isFavorite = isFavorite
@@ -187,13 +188,13 @@ object PlayerUtil {
187188
id = fileSource.toString(),
188189
uri = getPlaybackUri().toString(),
189190
name = path?.let { File(it).name } ?: "",
190-
mimeType = getMimeType(),
191+
mimeType = resolveMimeType(),
191192
contentLength = UNKNOWN_CONTENT_LENGTH,
192193
lastModified = sharedDate * SECOND_IN_MILLISECONDS,
193194
isFavorite = isFavorite
194195
)
195196

196-
private fun OCShare.getMimeType(): String = mimetype
197+
private fun OCShare.resolveMimeType(): String = mimetype
197198
?.takeIf { it.isNotEmpty() }
198199
?: path?.let { MimeTypeUtil.getMimeTypeFromPath(it) }
199200
?: ""
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
package com.nextcloud.utils.extensions
8+
9+
import com.owncloud.android.datamodel.OCFile
10+
import com.owncloud.android.utils.MimeTypeUtil
11+
12+
fun OCFile.resolveMimeType(): String = mimeType
13+
?.takeIf { it.isNotEmpty() }
14+
?: remotePath?.let { MimeTypeUtil.getMimeTypeFromPath(it) }
15+
?: ""

0 commit comments

Comments
 (0)