From 5a4aa388ae7f2c4905a6ae6719e63d00ba3e9948 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 12 May 2026 08:50:54 +0200 Subject: [PATCH 1/4] wip Signed-off-by: alperozturk96 --- .../e2e/E2EECounterFetchOperation.kt | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 app/src/main/java/com/owncloud/android/operations/e2e/E2EECounterFetchOperation.kt diff --git a/app/src/main/java/com/owncloud/android/operations/e2e/E2EECounterFetchOperation.kt b/app/src/main/java/com/owncloud/android/operations/e2e/E2EECounterFetchOperation.kt new file mode 100644 index 000000000000..aa9aa3da9704 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/operations/e2e/E2EECounterFetchOperation.kt @@ -0,0 +1,88 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.operations.e2e + +import android.content.Context +import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.network.ClientFactory +import com.nextcloud.utils.e2ee.E2EVersionHelper.isV2Plus +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.operations.RefreshFolderOperation +import com.owncloud.android.utils.theme.CapabilityUtils +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject + +class E2EECounterFetchOperation { + @Inject + lateinit var accountManager: UserAccountManager + + @Inject + lateinit var clientFactory: ClientFactory + + private var job: Job? = null + private val scope = CoroutineScope(Dispatchers.IO) + + companion object { + private const val TAG = "E2EECounterFetchOperation" + } + + fun execute( + file: OCFile, + storageManager: FileDataStorageManager, + context: Context, + onComplete: (Long) -> Unit + ) { + job = scope.launch(Dispatchers.IO) { + var counter = getE2ECounter(context, file) + + runCatching { + val client = clientFactory.create(accountManager.user) + val metadata = RefreshFolderOperation + .getDecryptedFolderMetadata(true, file, client, accountManager.user, context) + if (metadata is DecryptedFolderMetadataFile) { + counter = metadata.metadata.counter + file.setE2eCounter(metadata.metadata.counter) + storageManager.saveFile(file) + } + }.onFailure { e -> + Log_OC.e(TAG, "Error refreshing E2E counter: ${e.message}") + } + + withContext(Dispatchers.Main) { + onComplete(counter) + } + } + } + + private fun isEndToEndVersionAtLeastV2(context: Context): Boolean { + val capability = CapabilityUtils.getCapability(context) + return isV2Plus(capability) + } + + private fun getE2ECounter(context: Context, file: OCFile): Long { + var counter: Long = -1 + + if (isEndToEndVersionAtLeastV2(context)) { + counter = file.e2eCounter + 1 + } + + return counter + } + + fun stop() { + job?.cancel() + job = null + } +} From d1cb26ee453de1bf93cdc40a9ca2bf2ceb2f2968 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 12 May 2026 09:10:20 +0200 Subject: [PATCH 2/4] wip Signed-off-by: alperozturk96 --- .../operations/UploadFileOperation.java | 14 +--- .../e2e/E2EECounterFetchOperation.kt | 77 +++++++++++-------- .../fragment/FileDetailSharingFragment.java | 73 +++++++----------- 3 files changed, 73 insertions(+), 91 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java index 3a9d0bc0a766..d8bd55d96d7e 100644 --- a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java @@ -57,6 +57,7 @@ import com.owncloud.android.operations.common.SyncOperation; import com.owncloud.android.operations.e2e.E2EClientData; import com.owncloud.android.operations.e2e.E2EData; +import com.owncloud.android.operations.e2e.E2EECounterFetchOperation; import com.owncloud.android.operations.e2e.E2EFiles; import com.owncloud.android.operations.upload.UploadFileException; import com.owncloud.android.operations.upload.UploadFileOperationExtensionsKt; @@ -516,6 +517,7 @@ private RemoteOperationResult encryptedUpload(OwnCloudClient client, OCFile pare FileChannel channel = null; ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(getContext()); String publicKey = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.PUBLIC_KEY); + E2EECounterFetchOperation e2eeCounterFetchOperation = new E2EECounterFetchOperation(); try { result = checkConditions(e2eFiles.getOriginalFile()); @@ -524,7 +526,7 @@ private RemoteOperationResult encryptedUpload(OwnCloudClient client, OCFile pare return result; } - long counter = getE2ECounter(parentFile); + long counter = e2eeCounterFetchOperation.resolveE2EECounter(parentFile, getStorageManager(), mContext); try { token = getFolderUnlockTokenOrLockFolder(client, parentFile, counter); @@ -621,16 +623,6 @@ private boolean isEndToEndVersionAtLeastV2() { return E2EVersionHelper.INSTANCE.isV2Plus(capability); } - private long getE2ECounter(OCFile parentFile) { - long counter = -1; - - if (isEndToEndVersionAtLeastV2()) { - counter = parentFile.getE2eCounter() + 1; - } - - return counter; - } - private String getFolderUnlockTokenOrLockFolder(OwnCloudClient client, OCFile parentFile, long counter) throws UploadException { if (mFolderUnlockToken != null && !mFolderUnlockToken.isEmpty()) { Log_OC.d(TAG, "Reusing existing folder unlock token from previous upload attempt"); diff --git a/app/src/main/java/com/owncloud/android/operations/e2e/E2EECounterFetchOperation.kt b/app/src/main/java/com/owncloud/android/operations/e2e/E2EECounterFetchOperation.kt index aa9aa3da9704..e33444a7a747 100644 --- a/app/src/main/java/com/owncloud/android/operations/e2e/E2EECounterFetchOperation.kt +++ b/app/src/main/java/com/owncloud/android/operations/e2e/E2EECounterFetchOperation.kt @@ -9,6 +9,7 @@ package com.owncloud.android.operations.e2e import android.content.Context import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.di.Injectable import com.nextcloud.client.network.ClientFactory import com.nextcloud.utils.e2ee.E2EVersionHelper.isV2Plus import com.owncloud.android.datamodel.FileDataStorageManager @@ -19,70 +20,78 @@ import com.owncloud.android.operations.RefreshFolderOperation import com.owncloud.android.utils.theme.CapabilityUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject -class E2EECounterFetchOperation { +class E2EECounterFetchOperation: Injectable { @Inject lateinit var accountManager: UserAccountManager @Inject lateinit var clientFactory: ClientFactory - private var job: Job? = null - private val scope = CoroutineScope(Dispatchers.IO) + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) companion object { private const val TAG = "E2EECounterFetchOperation" } - fun execute( + fun fetchAsync( file: OCFile, storageManager: FileDataStorageManager, context: Context, onComplete: (Long) -> Unit ) { - job = scope.launch(Dispatchers.IO) { - var counter = getE2ECounter(context, file) - - runCatching { - val client = clientFactory.create(accountManager.user) - val metadata = RefreshFolderOperation - .getDecryptedFolderMetadata(true, file, client, accountManager.user, context) - if (metadata is DecryptedFolderMetadataFile) { - counter = metadata.metadata.counter - file.setE2eCounter(metadata.metadata.counter) - storageManager.saveFile(file) - } - }.onFailure { e -> - Log_OC.e(TAG, "Error refreshing E2E counter: ${e.message}") - } - + scope.launch { + val counter = resolveE2EECounter(file, storageManager, context) withContext(Dispatchers.Main) { onComplete(counter) } } } - private fun isEndToEndVersionAtLeastV2(context: Context): Boolean { - val capability = CapabilityUtils.getCapability(context) - return isV2Plus(capability) + fun resolveE2EECounter( + file: OCFile, + storageManager: FileDataStorageManager, + context: Context + ): Long { + return try { + val client = clientFactory.create(accountManager.user) + val metadata = RefreshFolderOperation + .getDecryptedFolderMetadata(true, file, client, accountManager.user, context) + if (metadata is DecryptedFolderMetadataFile) { + file.setE2eCounter(metadata.metadata.counter) + storageManager.saveFile(file) + Log_OC.i(TAG, "latest counter fetched") + metadata.metadata.counter + } else { + Log_OC.w(TAG, "local counter is used") + getFallbackCounter(context, file) + } + } catch (e: Exception) { + Log_OC.e(TAG, "error refreshing E2E counter: ${e.message}") + getFallbackCounter(context, file) + } } - private fun getE2ECounter(context: Context, file: OCFile): Long { - var counter: Long = -1 - - if (isEndToEndVersionAtLeastV2(context)) { - counter = file.e2eCounter + 1 - } + fun stop() { + scope.cancel() + } - return counter + // region private methods + private fun supportsE2EEv2(context: Context): Boolean { + val capability = CapabilityUtils.getCapability(context) + return isV2Plus(capability) } - fun stop() { - job?.cancel() - job = null + private fun getFallbackCounter(context: Context, file: OCFile): Long { + if (!supportsE2EEv2(context)) { + return -1 + } + return file.e2eCounter + 1 } + // endregion } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java index 424f54041076..a18c2442a329 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java @@ -50,16 +50,14 @@ import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.datamodel.SharesType; -import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile; import com.owncloud.android.lib.common.OwnCloudAccount; -import com.owncloud.android.lib.common.OwnCloudClient; import com.owncloud.android.lib.common.operations.RemoteOperationResult; import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.lib.resources.shares.OCShare; import com.owncloud.android.lib.resources.shares.ShareType; import com.owncloud.android.lib.resources.status.NextcloudVersion; import com.owncloud.android.lib.resources.status.OCCapability; -import com.owncloud.android.operations.RefreshFolderOperation; +import com.owncloud.android.operations.e2e.E2EECounterFetchOperation; import com.owncloud.android.providers.UsersAndGroupsSearchConfig; import com.owncloud.android.ui.activity.FileActivity; import com.owncloud.android.ui.activity.FileDisplayActivity; @@ -116,6 +114,8 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda private ShareeListAdapter externalShareeListAdapter; + private E2EECounterFetchOperation e2eeCounterFetchOperation; + @Inject UserAccountManager accountManager; @Inject ClientFactory clientFactory; @Inject ViewThemeUtils viewThemeUtils; @@ -155,6 +155,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { } fileActivity = (FileActivity) getActivity(); + e2eeCounterFetchOperation = new E2EECounterFetchOperation(); if (fileActivity == null) { throw new IllegalArgumentException("FileActivity may not be null"); @@ -272,6 +273,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, @Override public void onDestroyView() { super.onDestroyView(); + e2eeCounterFetchOperation.stop(); binding = null; } @@ -349,27 +351,28 @@ private void setupView() { binding.internalShareHeadline.setText(getResources().getString(R.string.internal_share_headline_end_to_end_encrypted)); binding.internalShareDescription.setVisibility(View.VISIBLE); binding.externalSharesHeadline.setText(getResources().getString(R.string.create_end_to_end_encrypted_share_title)); - - fetchE2EECounter(() -> { - if (binding == null) { - return; - } - - if (file.getE2eCounter() == -1) { - // V1 cannot share - binding.searchContainer.setVisibility(View.GONE); - binding.createLink.setVisibility(View.GONE); - } else { - binding.createLink.setText(R.string.add_new_secure_file_drop); - binding.searchView.setQueryHint(getResources().getString(R.string.secure_share_search)); - - if (file.isSharedViaLink()) { - binding.searchView.setQueryHint(getResources().getString(R.string.share_not_allowed_when_file_drop)); - binding.searchView.setInputType(InputType.TYPE_NULL); - toggleSearchViewEnable(binding.searchView, false); - } - } - }); + final Context context = requireContext(); + e2eeCounterFetchOperation.fetchAsync(file, fileDataStorageManager, context, counter -> { + if (binding == null) { + return Unit.INSTANCE; + } + + if (file.getE2eCounter() == -1) { + // V1 cannot share + binding.searchContainer.setVisibility(View.GONE); + binding.createLink.setVisibility(View.GONE); + } else { + binding.createLink.setText(R.string.add_new_secure_file_drop); + binding.searchView.setQueryHint(getResources().getString(R.string.secure_share_search)); + + if (file.isSharedViaLink()) { + binding.searchView.setQueryHint(getResources().getString(R.string.share_not_allowed_when_file_drop)); + binding.searchView.setInputType(InputType.TYPE_NULL); + toggleSearchViewEnable(binding.searchView, false); + } + } + return Unit.INSTANCE; + }); } else { binding.createLink.setText(R.string.create_link); binding.searchView.setQueryHint(getResources().getString(R.string.share_search_internal)); @@ -398,28 +401,6 @@ private void setupView() { ); } - private void fetchE2EECounter(Runnable onComplete) { - final Context context = requireContext(); - - new Thread(() -> { - try { - OwnCloudClient client = clientFactory.create(user); - Object metadata = RefreshFolderOperation.getDecryptedFolderMetadata(true, file, client, user, context); - if (metadata instanceof DecryptedFolderMetadataFile decryptedMetadata) { - file.setE2eCounter(decryptedMetadata.getMetadata().getCounter()); - fileDataStorageManager.saveFile(file); - } - } catch (Exception e) { - Log_OC.e(TAG, "Error refreshing E2E counter: " + e.getMessage()); - } - - final var activity = getActivity(); - if (activity != null) { - activity.runOnUiThread(onComplete); - } - }).start(); - } - private void checkShareViaUser() { if (!MDMConfig.INSTANCE.shareViaUser(requireContext())) { binding.searchContainer.setVisibility(View.GONE); From f8ae2dac8f058f38df8dbdd0f2b5b2713b1ab359 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 12 May 2026 09:23:05 +0200 Subject: [PATCH 3/4] wip Signed-off-by: alperozturk96 --- .../operations/UploadFileOperation.java | 3 +- .../e2e/E2EECounterFetchOperation.kt | 59 +++++++++---------- .../fragment/FileDetailSharingFragment.java | 3 +- 3 files changed, 32 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java index d8bd55d96d7e..058425c1c0e4 100644 --- a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java @@ -526,7 +526,8 @@ private RemoteOperationResult encryptedUpload(OwnCloudClient client, OCFile pare return result; } - long counter = e2eeCounterFetchOperation.resolveE2EECounter(parentFile, getStorageManager(), mContext); + long lastCounter = e2eeCounterFetchOperation.fetch(mContext, parentFile, getStorageManager(), user, client); + long counter = lastCounter + 1; try { token = getFolderUnlockTokenOrLockFolder(client, parentFile, counter); diff --git a/app/src/main/java/com/owncloud/android/operations/e2e/E2EECounterFetchOperation.kt b/app/src/main/java/com/owncloud/android/operations/e2e/E2EECounterFetchOperation.kt index e33444a7a747..cc85112319b6 100644 --- a/app/src/main/java/com/owncloud/android/operations/e2e/E2EECounterFetchOperation.kt +++ b/app/src/main/java/com/owncloud/android/operations/e2e/E2EECounterFetchOperation.kt @@ -8,13 +8,13 @@ package com.owncloud.android.operations.e2e import android.content.Context -import com.nextcloud.client.account.UserAccountManager -import com.nextcloud.client.di.Injectable +import com.nextcloud.client.account.User import com.nextcloud.client.network.ClientFactory import com.nextcloud.utils.e2ee.E2EVersionHelper.isV2Plus import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile +import com.owncloud.android.lib.common.OwnCloudClient import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.operations.RefreshFolderOperation import com.owncloud.android.utils.theme.CapabilityUtils @@ -24,15 +24,8 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import javax.inject.Inject - -class E2EECounterFetchOperation: Injectable { - @Inject - lateinit var accountManager: UserAccountManager - - @Inject - lateinit var clientFactory: ClientFactory +class E2EECounterFetchOperation { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) companion object { @@ -40,41 +33,45 @@ class E2EECounterFetchOperation: Injectable { } fun fetchAsync( + context: Context, file: OCFile, storageManager: FileDataStorageManager, - context: Context, + user: User, + clientFactory: ClientFactory, onComplete: (Long) -> Unit ) { scope.launch { - val counter = resolveE2EECounter(file, storageManager, context) + val client = clientFactory.create(user) + val counter = fetch(context, file, storageManager, user, client) withContext(Dispatchers.Main) { onComplete(counter) } } } - fun resolveE2EECounter( + fun fetch( + context: Context, file: OCFile, storageManager: FileDataStorageManager, - context: Context - ): Long { - return try { - val client = clientFactory.create(accountManager.user) - val metadata = RefreshFolderOperation - .getDecryptedFolderMetadata(true, file, client, accountManager.user, context) - if (metadata is DecryptedFolderMetadataFile) { - file.setE2eCounter(metadata.metadata.counter) - storageManager.saveFile(file) - Log_OC.i(TAG, "latest counter fetched") - metadata.metadata.counter - } else { - Log_OC.w(TAG, "local counter is used") - getFallbackCounter(context, file) - } - } catch (e: Exception) { - Log_OC.e(TAG, "error refreshing E2E counter: ${e.message}") - getFallbackCounter(context, file) + user: User, + client: OwnCloudClient + ): Long = try { + val metadata = RefreshFolderOperation + .getDecryptedFolderMetadata(true, file, client, user, context) + if (metadata is DecryptedFolderMetadataFile) { + val result = metadata.metadata.counter + file.setE2eCounter(result) + storageManager.saveFile(file) + Log_OC.i(TAG, "latest counter fetched, counter is: $result") + result + } else { + val result = getFallbackCounter(context, file) + Log_OC.w(TAG, "local counter is used, counter is: $result") + result } + } catch (e: Exception) { + Log_OC.e(TAG, "error refreshing E2E counter: ${e.message}") + getFallbackCounter(context, file) } fun stop() { diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java index a18c2442a329..78b649266df3 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java @@ -352,7 +352,8 @@ private void setupView() { binding.internalShareDescription.setVisibility(View.VISIBLE); binding.externalSharesHeadline.setText(getResources().getString(R.string.create_end_to_end_encrypted_share_title)); final Context context = requireContext(); - e2eeCounterFetchOperation.fetchAsync(file, fileDataStorageManager, context, counter -> { + + e2eeCounterFetchOperation.fetchAsync(context, file, fileDataStorageManager, user, clientFactory, counter -> { if (binding == null) { return Unit.INSTANCE; } From 8d94324f292e5562a58246f28d1d772cc55829cc Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 12 May 2026 09:48:47 +0200 Subject: [PATCH 4/4] wip Signed-off-by: alperozturk96 --- .../owncloud/android/ui/fragment/FileDetailSharingFragment.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java index 78b649266df3..23cb7f3b0df0 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java @@ -358,7 +358,7 @@ private void setupView() { return Unit.INSTANCE; } - if (file.getE2eCounter() == -1) { + if (counter == -1) { // V1 cannot share binding.searchContainer.setVisibility(View.GONE); binding.createLink.setVisibility(View.GONE);