Skip to content
Merged

refactor #17545

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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ class FileMenuFilterIT : AbstractIT() {
every { mockComponentsGetter.operationsServiceBinder } returns mockOperationsServiceBinder
every { mockStorageManager.getFileById(any()) } returns OCFile("/")
every { mockStorageManager.getFolderContent(any(), any()) } returns ArrayList<OCFile>()
every { mockStorageManager.isReadOnly(any()) } returns false
every { mockArbitraryDataProvider.getValue(any<User>(), any()) } returns ""
editorUtils = EditorUtils(mockArbitraryDataProvider)
}
Expand Down Expand Up @@ -363,6 +364,63 @@ class FileMenuFilterIT : AbstractIT() {
}
}

@Test
fun filter_readOnlyFile_hidesModifyingActions() {
configureCapability(
OCCapability().apply {
endToEndEncryption = CapabilityBooleanType.TRUE
filesLockingVersion = "1.0"
}
)

every { mockStorageManager.isReadOnly(any()) } returns true

val file = OCFile("/readOnly.txt").apply {
permissions = FULL_PERMISSIONS
}

launchActivity<TestActivity>().use {
it.onActivity { activity ->
val filterFactory = FileMenuFilter.Factory(mockStorageManager, activity, editorUtils)

val toHide = filterFactory
.newInstance(file, mockComponentsGetter, true, user)
.getToHide(false)

READ_ONLY_HIDDEN_ACTIONS.forEach { action ->
assertTrue(toHide.contains(action))
}
}
}
}

@Test
fun filter_writableFile_keepsModifyingActions() {
configureCapability(
OCCapability().apply {
filesLockingVersion = "1.0"
}
)

val file = OCFile("/writable.txt").apply {
permissions = FULL_PERMISSIONS
}

launchActivity<TestActivity>().use {
it.onActivity { activity ->
val filterFactory = FileMenuFilter.Factory(mockStorageManager, activity, editorUtils)

val toHide = filterFactory
.newInstance(file, mockComponentsGetter, true, user)
.getToHide(false)

WRITABLE_VISIBLE_ACTIONS.forEach { action ->
assertFalse(toHide.contains(action))
}
}
}
}

private data class ExpectedLockVisibilities(val lockFile: Boolean, val unlockFile: Boolean)

private fun configureCapability(capability: OCCapability) {
Expand Down Expand Up @@ -398,6 +456,29 @@ class FileMenuFilterIT : AbstractIT() {
}

companion object {
private const val FULL_PERMISSIONS = "RGDNVW"

private val READ_ONLY_HIDDEN_ACTIONS = listOf(
R.id.action_remove_file,
R.id.action_rename_file,
R.id.action_move_or_copy,
R.id.action_edit,
R.id.action_encrypted,
R.id.action_unset_encrypted,
R.id.action_lock_file,
R.id.action_unlock_file,
R.id.action_favorite,
R.id.action_unset_favorite
)

private val WRITABLE_VISIBLE_ACTIONS = listOf(
R.id.action_remove_file,
R.id.action_rename_file,
R.id.action_move_or_copy,
R.id.action_lock_file,
R.id.action_favorite
)

private const val OFFICE_MIMETYPE =
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"

Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ import com.owncloud.android.db.ProviderMeta
// manual migration used for 99 to 100
AutoMigration(from = 100, to = 101, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class),
AutoMigration(from = 101, to = 102, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class),
AutoMigration(from = 102, to = 103, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class)
AutoMigration(from = 102, to = 103, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class),
AutoMigration(from = 103, to = 104)
],
exportSchema = true
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,4 +188,7 @@ interface FileDao {

@Query("DELETE FROM filelist WHERE file_owner = :fileOwner AND path = :remotePath")
fun deleteFileByRemotePath(fileOwner: String, remotePath: String): Int

@Query("UPDATE filelist SET is_read_only = :readOnly WHERE file_owner = :fileOwner AND path = :path")
fun setReadOnly(fileOwner: String, path: String, readOnly: Int): Int
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,5 +121,7 @@ data class FileEntity(
@ColumnInfo(name = ProviderTableMeta.FILE_INTERNAL_TWO_WAY_SYNC_RESULT)
val internalTwoWaySyncResult: String?,
@ColumnInfo(name = ProviderTableMeta.FILE_UPLOADED)
val uploaded: Long?
val uploaded: Long?,
@ColumnInfo(name = ProviderTableMeta.FILE_IS_READ_ONLY)
val isReadOnly: Int?
)
26 changes: 24 additions & 2 deletions app/src/main/java/com/nextcloud/client/di/AppModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,21 @@
import com.nextcloud.client.migrations.MigrationsManager;
import com.nextcloud.client.migrations.MigrationsManagerImpl;
import com.nextcloud.client.network.ClientFactory;
import com.nextcloud.client.network.ConnectivityService;
import com.nextcloud.client.notifications.AppNotificationManager;
import com.nextcloud.client.notifications.AppNotificationManagerImpl;
import com.nextcloud.client.preferences.AppPreferences;
import com.nextcloud.client.utils.Throttler;
import com.owncloud.android.providers.UsersAndGroupsSearchConfig;
import com.nextcloud.utils.e2ee.E2EEActionResolver;
import com.nextcloud.utils.e2ee.E2EEKeyInspector;
import com.nextcloud.utils.thumbnail.FolderThumbnailGenerator;
import com.owncloud.android.authentication.PassCodeManager;
import com.owncloud.android.datamodel.ArbitraryDataProvider;
import com.owncloud.android.datamodel.ArbitraryDataProviderImpl;
import com.owncloud.android.datamodel.FileDataStorageManager;
import com.owncloud.android.datamodel.SyncedFolderProvider;
import com.owncloud.android.datamodel.UploadsStorageManager;
import com.owncloud.android.providers.UsersAndGroupsSearchConfig;
import com.owncloud.android.ui.activities.data.activities.ActivitiesRepository;
import com.owncloud.android.ui.activities.data.activities.ActivitiesServiceApi;
import com.owncloud.android.ui.activities.data.activities.ActivitiesServiceApiImpl;
Expand All @@ -57,7 +61,6 @@
import com.owncloud.android.ui.activities.data.files.FilesServiceApiImpl;
import com.owncloud.android.ui.activities.data.files.RemoteFilesRepository;
import com.owncloud.android.ui.dialog.setupEncryption.CertificateValidator;
import com.nextcloud.utils.thumbnail.FolderThumbnailGenerator;
import com.owncloud.android.utils.theme.ViewThemeUtils;

import org.greenrobot.eventbus.EventBus;
Expand Down Expand Up @@ -280,4 +283,23 @@ FolderThumbnailGenerator folderThumbnailGenerator(
return new FolderThumbnailGenerator(appPreferences, viewThemeUtils, context, accountManager);
}

@Provides
E2EEKeyInspector e2eeKeyInspector(
Context context,
FileDataStorageManager fileDataStorageManager,
CertificateValidator certificateValidator,
ArbitraryDataProvider arbitraryDataProvider,
UserAccountManager accountManager) {
return new E2EEKeyInspector(context, fileDataStorageManager, certificateValidator, arbitraryDataProvider, accountManager);
}

@Provides
E2EEActionResolver e2eeActionResolver(
FileDataStorageManager fileDataStorageManager,
ArbitraryDataProvider arbitraryDataProvider,
UserAccountManager accountManager,
ConnectivityService connectivityService,
E2EEKeyInspector e2eeKeyInspector) {
return new E2EEActionResolver(fileDataStorageManager, arbitraryDataProvider, accountManager, connectivityService, e2eeKeyInspector);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.nextcloud.client.network.ConnectivityService
import com.nextcloud.model.OfflineOperationType
import com.nextcloud.model.WorkerState
import com.nextcloud.model.WorkerStateObserver
import com.nextcloud.utils.extensions.isNetworkAndServerAvailableSuspended
import com.owncloud.android.datamodel.FileDataStorageManager
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.lib.common.OwnCloudClient
Expand All @@ -35,8 +36,6 @@ import com.owncloud.android.utils.MimeTypeUtil
import com.owncloud.android.utils.theme.ViewThemeUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

private typealias OfflineOperationResult = Pair<RemoteOperationResult<*>?, RemoteOperation<*>?>?

Expand Down Expand Up @@ -67,7 +66,7 @@ class OfflineOperationsWorker(
Log_OC.d(TAG, "[$jobName] OfflineOperationsWorker started for user: ${user.accountName}")

// check network connection
if (!isNetworkAndServerAvailable()) {
if (!connectivityService.isNetworkAndServerAvailableSuspended()) {
Log_OC.w(TAG, "⚠️ No internet/server connection. Retrying later...")
return@withContext Result.retry()
}
Expand Down Expand Up @@ -161,12 +160,6 @@ class OfflineOperationsWorker(
}
// endregion

private suspend fun isNetworkAndServerAvailable(): Boolean = suspendCoroutine { continuation ->
connectivityService.isNetworkAndServerAvailable { result ->
continuation.resume(result)
}
}

// region Operation Execution
@Suppress("ComplexCondition", "LongMethod")
private suspend fun executeOperation(
Expand Down
90 changes: 90 additions & 0 deletions app/src/main/java/com/nextcloud/utils/e2ee/E2EEActionResolver.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

package com.nextcloud.utils.e2ee

import com.nextcloud.client.account.UserAccountManager
import com.nextcloud.client.di.Injectable
import com.nextcloud.client.network.ConnectivityService
import com.nextcloud.utils.e2ee.model.E2EEKeyCheck
import com.nextcloud.utils.extensions.isNetworkAndServerAvailableSuspended
import com.owncloud.android.datamodel.ArbitraryDataProvider
import com.owncloud.android.datamodel.FileDataStorageManager
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.ui.dialog.setupEncryption.model.DownloadKeyResult
import com.owncloud.android.utils.EncryptionUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject

@Suppress("LongParameterList")
class E2EEActionResolver @Inject constructor(
private val storageManager: FileDataStorageManager,
private val arbitraryDataProvider: ArbitraryDataProvider,
private val accountManager: UserAccountManager,
private val connectivityService: ConnectivityService,
private val inspector: E2EEKeyInspector
) : Injectable {

companion object {
private const val TAG = "E2EEActionResolver"
}

suspend fun markFolderReadOnly(file: OCFile) = withContext(Dispatchers.IO) {
storageManager.setReadOnly(file, true)
}

suspend fun checkFolderMetadataKey(file: OCFile): Boolean = withContext(Dispatchers.IO) {
val capability = storageManager.getCapability(accountManager.user)
val canDecrypt = inspector.canDecryptFolderMetadata(file, capability)
storageManager.setReadOnly(file, !canDecrypt)
return@withContext canDecrypt
}

suspend fun checkKeys(): E2EEKeyCheck = withContext(Dispatchers.IO) { resolveKeyCheck() }

private suspend fun resolveKeyCheck(): E2EEKeyCheck {
val keysAbsentLocally = inspector.isLocalKeysAbsent()

if (!inspector.fetchCapabilities()) {
return if (connectivityService.isNetworkAndServerAvailableSuspended()) {
E2EEKeyCheck.CHECK_FAILED
} else {
E2EEKeyCheck.NO_NETWORK
}
}

val capability = storageManager.getCapability(accountManager.user)
val keysExistOnServer = capability.endToEndEncryptionKeysExist

val result = when {
keysExistOnServer.isUnknown || capability.endToEndEncryption.isUnknown -> E2EEKeyCheck.E2EE_UNAVAILABLE
keysAbsentLocally && keysExistOnServer.isTrue -> E2EEKeyCheck.ONLY_ON_SERVER
keysAbsentLocally -> E2EEKeyCheck.MISSING_EVERYWHERE
!keysExistOnServer.isTrue -> E2EEKeyCheck.ONLY_ON_DEVICE
else -> compareKeys()
}

Log_OC.d(TAG, "e2ee key check result: $result")

return result
}

private suspend fun compareKeys(): E2EEKeyCheck {
val storedPublicKey = arbitraryDataProvider.getValue(accountManager.user, EncryptionUtils.PUBLIC_KEY)

return when (val result = inspector.compareWithServerKey(storedPublicKey)) {
is DownloadKeyResult.CompareKeys ->
if (result.same) E2EEKeyCheck.SAME_AS_SERVER else E2EEKeyCheck.DIFFERS_FROM_SERVER

is DownloadKeyResult.NoServerKey -> E2EEKeyCheck.ONLY_ON_DEVICE

else -> E2EEKeyCheck.CHECK_FAILED
}
}
}
50 changes: 50 additions & 0 deletions app/src/main/java/com/nextcloud/utils/e2ee/E2EEDialogPresenter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

package com.nextcloud.utils.e2ee

import com.nextcloud.utils.e2ee.model.E2EEDialog
import com.owncloud.android.R
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.ui.dialog.ConfirmationDialogFragment
import com.owncloud.android.ui.dialog.ConfirmationDialogFragment.ConfirmationDialogFragmentListener
import com.owncloud.android.ui.fragment.OCFileListFragment

class E2EEDialogPresenter(private val fragment: OCFileListFragment) {

companion object {
private const val TAG = "E2EEDialogPresenter"
private const val ENCRYPTION_KEY_ALERT_DIALOG_TAG = "ENCRYPTION_KEY_HANDLER_DIALOG"
private const val NO_BUTTON = -1
}

fun show(dialog: E2EEDialog) {
if (fragment.parentFragmentManager.findFragmentByTag(ENCRYPTION_KEY_ALERT_DIALOG_TAG) != null) {
return
}

ConfirmationDialogFragment
.newInstance(
titleResId = dialog.titleId,
titleIconId = R.drawable.ic_lock_open_white,
messageResId = dialog.descriptionId,
positiveButtonTextId = R.string.common_ok,
neutralButtonTextId = NO_BUTTON,
negativeButtonTextId = NO_BUTTON,
messageArguments = null
).apply {
setOnConfirmationListener(dismissListener(dialog))
}.show(fragment.parentFragmentManager, ENCRYPTION_KEY_ALERT_DIALOG_TAG)
}

private fun dismissListener(dialog: E2EEDialog): ConfirmationDialogFragmentListener =
object : ConfirmationDialogFragmentListener {
override fun onConfirmation(callerTag: String?) = Log_OC.d(TAG, "$dialog acknowledged")
override fun onNeutral(callerTag: String?) = Unit
override fun onCancel(callerTag: String?) = Unit
}
}
Loading
Loading