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
1,388 changes: 1,388 additions & 0 deletions app/schemas/com.nextcloud.client.database.NextcloudDatabase/106.json

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions app/src/androidTest/java/com/nextcloud/test/FlakyTestFilter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.test

import org.junit.runner.Description
import org.junit.runner.manipulation.Filter

class FlakyTestFilter : Filter() {
override fun shouldRun(description: Description): Boolean = when {
description.isTest -> !description.isFlaky()
else -> description.children.any { shouldRun(it) }
}

override fun describe(): String = "skip tests annotated with @Flaky"

private fun Description.isFlaky(): Boolean {
val onMethod = getAnnotation(Flaky::class.java) != null
val onClass = testClass?.isAnnotationPresent(Flaky::class.java) == true
return onMethod || onClass
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.Before
import org.junit.Test
import java.io.IOException
Expand Down Expand Up @@ -50,7 +51,7 @@ class GrantFolderExistenceTests : AbstractOnServerIT() {
assertTrue("month folder should exist on server", existsOnServer(monthFolder))
assertNotNull("month folder should be cached locally", storageManager.getFileByDecryptedRemotePath(monthFolder))

removeYearFolderOnServerOnly()
removeOnServer(yearFolder)

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

assertTrue(
"root folder should be removed",
RemoveFileRemoteOperation(root).execute(client).isSuccess
)
removeOnServer(root)
assertFalse(existsOnServer(root))

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

private fun removeYearFolderOnServerOnly() {
assertTrue(
"year folder should be removed",
RemoveFileRemoteOperation(yearFolder).execute(client).isSuccess
)
private fun removeOnServer(remotePath: String) {
// the server keeps a transient lock on a just uploaded file, so a DELETE on one of its
// parent folders answers 423 until that lock expires
repeat(REMOVE_ATTEMPTS) {
if (RemoveFileRemoteOperation(remotePath).execute(client).isSuccess) {
return
}

shortSleep()
}

fail("$remotePath should be removed on server")
}

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

companion object {
private const val FILE_LINE_COUNT = 100
private const val REMOVE_ATTEMPTS = 5
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@ package com.owncloud.android.providers

import android.provider.DocumentsContract
import androidx.documentfile.provider.DocumentFile
import com.nextcloud.client.account.UserAccountManagerImpl
import com.nextcloud.client.jobs.upload.FileUploadHelper
import com.nextcloud.test.RandomStringGenerator
import com.owncloud.android.AbstractOnServerIT
import com.owncloud.android.R
import com.owncloud.android.datamodel.OCFile.ROOT_PATH
import com.owncloud.android.datamodel.UploadsStorageManager
import com.owncloud.android.datamodel.UploadsStorageManager.UploadStatus
import com.owncloud.android.db.OCUpload
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.providers.DocumentsProviderUtils.assertExistsOnServer
import com.owncloud.android.providers.DocumentsProviderUtils.assertListFilesEquals
Expand All @@ -31,12 +36,16 @@ import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.Before
import org.junit.Test
import kotlin.random.Random

private const val MAX_FILE_NAME_LENGTH = 225

// the provider can only enqueue the upload worker, so give it a generous budget before giving up
private const val UPLOAD_POLL_ATTEMPTS = 45

class DocumentsStorageProviderIT : AbstractOnServerIT() {

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

private val uploadsStorageManager = UploadsStorageManager(
UserAccountManagerImpl.fromContext(context),
contentResolver
)

@Before
fun before() {
// an upload left over by a previous test fails once its local file is gone, and WorkManager
// then drops everything appended behind it in the upload chain, including our own uploads
FileUploadHelper.instance().cancel(user.accountName)

// DocumentsProvider#onCreate() is called when the application is started
// which is *after* AbstractOnServerIT adds the accounts (when the app is freshly installed).
// So we need to query our roots here to ensure that the internal storage map is initialized.
Expand Down Expand Up @@ -211,11 +229,7 @@ class DocumentsStorageProviderIT : AbstractOnServerIT() {
it!!.write(content1)
}

// refresh
while (file1.getOCFile(storageManager)!!.etagOnServer == createdETag) {
shortSleep()
rootDir.listFiles()
}
awaitUploadedToServer(file1, createdETag)

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

Expand Down Expand Up @@ -250,11 +264,7 @@ class DocumentsStorageProviderIT : AbstractOnServerIT() {
it!!.write(content1)
}

// refresh
while (file1.getOCFile(storageManager)!!.etagOnServer == createdETag) {
shortSleep()
rootDir.listFiles()
}
awaitUploadedToServer(file1, createdETag)

val content2 = "new content".toByteArray()

Expand All @@ -266,4 +276,31 @@ class DocumentsStorageProviderIT : AbstractOnServerIT() {
val bytes = contentResolver.openInputStream(file1.uri)?.readBytes() ?: ByteArray(0)
assertEquals(String(content2), String(bytes))
}

private fun awaitUploadedToServer(file: DocumentFile, etagBeforeUpload: String) {
val remotePath = file.getOCFile(storageManager)!!.remotePath

repeat(UPLOAD_POLL_ATTEMPTS) {
if (file.getOCFile(storageManager)!!.etagOnServer != etagBeforeUpload) {
return
}

failedUpload(remotePath)?.let {
fail("upload of $remotePath failed with ${it.lastResult}")
}

shortSleep()
rootDir.listFiles()
}

fail("upload of $remotePath did not finish, stored uploads: ${describeUploads()}")
}

private fun failedUpload(remotePath: String): OCUpload? = uploadsStorageManager
.getUploadsForAccount(user.accountName)
.firstOrNull { it.remotePath == remotePath && it.uploadStatus == UploadStatus.UPLOAD_FAILED }

private fun describeUploads(): String = uploadsStorageManager
.getUploadsForAccount(user.accountName)
.joinToString { "${it.remotePath} is ${it.uploadStatus} with ${it.lastResult}" }
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class UnifiedSearchFragmentIT : AbstractIT() {
scenario.onActivity { activity ->
val sut = UnifiedSearchFragment.newInstance(null, null, "/")
val testViewModel = UnifiedSearchViewModel(activity.application)
testViewModel.setCurrentAccountProvider(activity.userAccountManager)
testViewModel.setConnectivityService(activity.connectivityServiceMock)
val localRepository = UnifiedSearchFakeRepository()
testViewModel.setRepository(localRepository)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ import com.owncloud.android.db.ProviderMeta
AutoMigration(from = 101, to = 102, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class),
AutoMigration(from = 102, to = 103, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class),
AutoMigration(from = 103, to = 104),
AutoMigration(from = 104, to = 105)
AutoMigration(from = 104, to = 105),
AutoMigration(from = 105, to = 106, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class)
],
exportSchema = true
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,9 @@ data class CapabilityEntity(
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_MOD_REWRITE_WORKING)
val modRewriteWorking: Int?,
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_CHUNKED_UPLOAD_MAX_SIZE)
val chunkedUploadMaxSize: Long?
val chunkedUploadMaxSize: Long?,
@ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SHARING_JSON)
val sharingJson: String?
)

@Suppress("LongMethod", "ReturnCount")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import com.nextcloud.client.player.model.file.PlaybackFiles
import com.nextcloud.client.player.model.file.PlaybackFilesComparator
import com.nextcloud.client.player.model.file.PlaybackFilesRepository
import com.nextcloud.client.player.util.PlayerUtil.toPlaybackFile
import com.nextcloud.utils.extensions.resolveMimeType
import com.owncloud.android.datamodel.OCFile
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -68,7 +69,7 @@ class PlayerLauncher @Inject constructor(
}

private suspend fun prepareQueue(file: OCFile, collection: PlaybackCollection): PlaybackFileType {
val fileType = PlaybackFileType.ofMimeType(file.mimeType)
val fileType = PlaybackFileType.ofMimeType(file.resolveMimeType())
playbackResumptionConfigStore.saveConfig(file.localId.toString(), file.parentId, fileType, collection)

playbackModel.start()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import com.nextcloud.client.player.model.state.PlaybackState
import com.nextcloud.client.player.model.state.PlayerState
import com.nextcloud.client.player.model.state.RepeatMode
import com.nextcloud.client.player.model.state.VideoSize
import com.nextcloud.utils.extensions.resolveMimeType
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.lib.resources.shares.OCShare
import com.owncloud.android.utils.MimeTypeUtil
Expand Down Expand Up @@ -177,7 +178,7 @@ object PlayerUtil {
id = localId.toString(),
uri = getPlaybackUri().toString(),
name = fileName,
mimeType = mimeType,
mimeType = resolveMimeType(),
contentLength = fileLength,
lastModified = modificationTimestamp,
isFavorite = isFavorite
Expand All @@ -187,13 +188,13 @@ object PlayerUtil {
id = fileSource.toString(),
uri = getPlaybackUri().toString(),
name = path?.let { File(it).name } ?: "",
mimeType = getMimeType(),
mimeType = resolveMimeType(),
contentLength = UNKNOWN_CONTENT_LENGTH,
lastModified = sharedDate * SECOND_IN_MILLISECONDS,
isFavorite = isFavorite
)

private fun OCShare.getMimeType(): String = mimetype
private fun OCShare.resolveMimeType(): String = mimetype
?.takeIf { it.isNotEmpty() }
?: path?.let { MimeTypeUtil.getMimeTypeFromPath(it) }
?: ""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.utils.extensions

import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.utils.MimeTypeUtil

fun OCFile.resolveMimeType(): String = mimeType
?.takeIf { it.isNotEmpty() }
?: remotePath?.let { MimeTypeUtil.getMimeTypeFromPath(it) }
?: ""
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
package com.nextcloud.utils.extensions

import android.content.Context
import com.nextcloud.android.common.ui.network.auth.ServerCredentials
import com.nextcloud.common.NextcloudClient
import com.owncloud.android.lib.common.OwnCloudClient
import com.owncloud.android.lib.common.OwnCloudClientFactory
Expand All @@ -34,3 +35,9 @@ fun OwnCloudClient.getVideoPreviewEndpoint(localFileId: Long, size: Int): String
localFileId +
"&x=" + size + "&y=" + size +
"&a=1&forceIcon=0"

/**
* Used in Android Common
*/
fun OwnCloudClient.toServerCredentials(baseURL: String): ServerCredentials =
ServerCredentials(baseURL, userIdPlain, credentials.authToken)
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

package com.nextcloud.utils.extensions

import com.nextcloud.android.common.ui.network.auth.ServerCredentials
import com.nextcloud.android.common.ui.network.http.NextcloudHttpClient
import com.nextcloud.android.common.ui.network.model.dataOrElse
import com.nextcloud.android.common.ui.share.repository.ShareRemoteRepository
import com.owncloud.android.lib.common.utils.Log_OC

private const val TAG = "ServerCredentialsExtensions"

suspend fun ServerCredentials.supportsUnifiedShare(): Boolean {
val capabilities = ShareRemoteRepository(NextcloudHttpClient.create(this))
.fetchSharingCapabilities()
.dataOrElse { Log_OC.e(TAG, "Failed to fetch sharing capabilities") }

return capabilities?.isUnifiedShareEnabled == true
}
27 changes: 27 additions & 0 deletions app/src/main/java/com/nextcloud/utils/extensions/UserExtensions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

package com.nextcloud.utils.extensions

import com.nextcloud.android.common.ui.network.auth.ServerCredentials
import com.nextcloud.client.account.User
import com.owncloud.android.MainApp
import com.owncloud.android.lib.common.OwnCloudClientManagerFactory
import com.owncloud.android.lib.common.utils.Log_OC

private const val TAG = "UserExtensions"

@Suppress("TooGenericExceptionCaught")
fun User.toServerCredentials(): ServerCredentials? = try {
OwnCloudClientManagerFactory
.getDefaultSingleton()
.getClientFor(toOwnCloudAccount(), MainApp.getAppContext())
.toServerCredentials(server.uri.toString())
} catch (e: Exception) {
Log_OC.e(TAG, "Failed to create client for $accountName", e)
null
}
Original file line number Diff line number Diff line change
Expand Up @@ -2373,6 +2373,8 @@ private ContentValues createContentValues(String accountName, OCCapability capab

contentValues.put(ProviderTableMeta.CAPABILITIES_CHUNKED_UPLOAD_MAX_SIZE, capability.getChunkedUploadMaxSize());

contentValues.put(ProviderTableMeta.CAPABILITIES_SHARING_JSON, capability.getSharingJson());

return contentValues;
}

Expand Down Expand Up @@ -2576,6 +2578,8 @@ private OCCapability createCapabilityInstance(Cursor cursor) {

capability.setChunkedUploadMaxSize(
getLong(cursor, ProviderTableMeta.CAPABILITIES_CHUNKED_UPLOAD_MAX_SIZE));

capability.setSharingJson(getString(cursor, ProviderTableMeta.CAPABILITIES_SHARING_JSON));
}

return capability;
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/java/com/owncloud/android/db/ProviderMeta.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
*/
public class ProviderMeta {
public static final String DB_NAME = "filelist";
public static final int DB_VERSION = 105;
public static final int DB_VERSION = 106;

private ProviderMeta() {
// No instance
Expand Down Expand Up @@ -299,6 +299,7 @@ static public class ProviderTableMeta implements BaseColumns {
public static final String CAPABILITIES_CLIENT_INTEGRATION_JSON = "client_integration_json";
public static final String CAPABILITIES_MOD_REWRITE_WORKING = "mod_rewrite_working";
public static final String CAPABILITIES_CHUNKED_UPLOAD_MAX_SIZE = "chunked_upload_max_size";
public static final String CAPABILITIES_SHARING_JSON = "sharing_json";

//Columns of Uploads table
public static final String UPLOADS_LOCAL_PATH = "local_path";
Expand Down
Loading
Loading