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..058425c1c0e4 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,8 @@ private RemoteOperationResult encryptedUpload(OwnCloudClient client, OCFile pare return result; } - long counter = getE2ECounter(parentFile); + long lastCounter = e2eeCounterFetchOperation.fetch(mContext, parentFile, getStorageManager(), user, client); + long counter = lastCounter + 1; try { token = getFolderUnlockTokenOrLockFolder(client, parentFile, counter); @@ -621,16 +624,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 new file mode 100644 index 000000000000..cc85112319b6 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/operations/e2e/E2EECounterFetchOperation.kt @@ -0,0 +1,94 @@ +/* + * 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.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 +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class E2EECounterFetchOperation { + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + + companion object { + private const val TAG = "E2EECounterFetchOperation" + } + + fun fetchAsync( + context: Context, + file: OCFile, + storageManager: FileDataStorageManager, + user: User, + clientFactory: ClientFactory, + onComplete: (Long) -> Unit + ) { + scope.launch { + val client = clientFactory.create(user) + val counter = fetch(context, file, storageManager, user, client) + withContext(Dispatchers.Main) { + onComplete(counter) + } + } + } + + fun fetch( + context: Context, + file: OCFile, + storageManager: FileDataStorageManager, + 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() { + scope.cancel() + } + + // region private methods + private fun supportsE2EEv2(context: Context): Boolean { + val capability = CapabilityUtils.getCapability(context) + return isV2Plus(capability) + } + + 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..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 @@ -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,29 @@ 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(context, file, fileDataStorageManager, user, clientFactory, counter -> { + if (binding == null) { + return Unit.INSTANCE; + } + + if (counter == -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 +402,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);