Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
4f602fe
feat(unified-share): ui
alperozturk96 Apr 16, 2026
280bee1
feat(unified-share): ui
alperozturk96 Apr 16, 2026
12f3784
add todos
alperozturk96 Apr 17, 2026
0e4fc70
add todos
alperozturk96 Apr 17, 2026
b47365b
add todos
alperozturk96 Apr 17, 2026
8cd27d3
wip
alperozturk96 Apr 17, 2026
3de6a81
wip
alperozturk96 Apr 17, 2026
1c26ca5
wip
alperozturk96 Apr 17, 2026
4a1d21e
wip
alperozturk96 Apr 17, 2026
13d118f
wip
alperozturk96 May 5, 2026
f81c9f3
wip
alperozturk96 May 5, 2026
a8d64cc
wip
alperozturk96 May 6, 2026
74a4087
wip
alperozturk96 Jun 9, 2026
3426421
use getMaxCompatibleE2EEVersion
alperozturk96 Jun 12, 2026
3208422
wip
alperozturk96 Jun 17, 2026
70782cf
wip
alperozturk96 Jul 2, 2026
1a384ce
wip
alperozturk96 Jul 2, 2026
d29759b
wip
alperozturk96 Jul 3, 2026
c507443
wip
alperozturk96 Jul 6, 2026
0ed7bb8
wip
alperozturk96 Jul 6, 2026
408768f
wip
alperozturk96 Jul 8, 2026
77b3e0b
wip
alperozturk96 Jul 9, 2026
8e41899
wip
alperozturk96 Jul 14, 2026
b7c600c
wip
alperozturk96 Jul 14, 2026
3a7d112
wip
alperozturk96 Jul 22, 2026
c714e9d
wip
alperozturk96 Jul 29, 2026
15588ca
wip
alperozturk96 Jul 29, 2026
d4f2f60
wip
alperozturk96 Aug 10, 2026
c48d46c
wip
alperozturk96 Aug 10, 2026
e2d0ef9
wip
alperozturk96 Aug 10, 2026
e176697
wip
alperozturk96 Aug 20, 2026
a08bccf
wip
alperozturk96 Aug 21, 2026
abadf12
wip
alperozturk96 Aug 21, 2026
8508b6e
wip
alperozturk96 Sep 1, 2026
cd4a3b0
wip
alperozturk96 Sep 3, 2026
67a1dbd
wip
alperozturk96 Sep 3, 2026
2c8e25e
wip
alperozturk96 Sep 3, 2026
1421685
wip
alperozturk96 Sep 3, 2026
aeb26f1
wip
alperozturk96 Sep 4, 2026
c54a4c4
wip
alperozturk96 Sep 4, 2026
dc9137b
wip
alperozturk96 Sep 4, 2026
714d66b
wip
alperozturk96 Sep 4, 2026
47ff888
wip
alperozturk96 Sep 8, 2026
bbea7ca
wip
alperozturk96 Sep 8, 2026
78701b6
wip
alperozturk96 Sep 8, 2026
6d8c69f
wip
alperozturk96 Sep 8, 2026
9c47dba
wip
alperozturk96 Sep 8, 2026
8c113d1
wip
alperozturk96 Sep 9, 2026
5fe394d
use localId, not remoteId
tobiasKaminsky Sep 9, 2026
067d296
wip
alperozturk96 Sep 9, 2026
b26126e
wip
alperozturk96 Sep 9, 2026
eefd06d
wip
alperozturk96 Sep 9, 2026
6011717
wip
alperozturk96 Sep 9, 2026
01366e9
wip
alperozturk96 Sep 9, 2026
17a7215
wip
alperozturk96 Sep 9, 2026
cd702b0
fix app launch crash for some specific files
alperozturk96 Sep 9, 2026
f35583f
fix app launch crash for some specific files
alperozturk96 Sep 9, 2026
da6d07d
wip
alperozturk96 Sep 10, 2026
f5d1e32
wip
alperozturk96 Sep 10, 2026
596d270
wip
alperozturk96 Sep 10, 2026
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