From 32a9d9f6f248708a98b0daf9a626c7ecfd5f3cec Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 1 Jul 2026 14:25:35 +0200 Subject: [PATCH 1/6] fix(e2ee): refresh during crud operations Signed-off-by: alperozturk96 --- .../com/owncloud/android/utils/EncryptionUtilsV2IT.kt | 11 +++-------- .../android/operations/CreateFolderOperation.java | 11 +++++++---- .../operations/RemoveRemoteEncryptedFileOperation.kt | 7 +++++-- .../android/operations/UploadFileOperation.java | 9 ++++++--- .../com/owncloud/android/utils/EncryptionUtilsV2.kt | 8 ++------ 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsV2IT.kt b/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsV2IT.kt index 8e09ee62dbcb..8d658c5fab7d 100644 --- a/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsV2IT.kt +++ b/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsV2IT.kt @@ -1,6 +1,7 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2023 Nextcloud GmbH * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only @@ -341,7 +342,6 @@ class EncryptionUtilsV2IT : EncryptionIT() { @Test fun addFolder() { - val folder = OCFile("/e/") val enc1 = MockUser("enc1", "Nextcloud") val metadataFile = generateDecryptedFolderMetadataFile(enc1, enc1Cert) assertEquals(2, metadataFile.metadata.files.size) @@ -350,9 +350,7 @@ class EncryptionUtilsV2IT : EncryptionIT() { val updatedMetadata = encryptionUtilsV2.addFolderToMetadata( EncryptionUtils.generateUid(), "new subfolder", - metadataFile, - folder, - storageManager + metadataFile ) assertEquals(2, updatedMetadata.metadata.files.size) @@ -361,7 +359,6 @@ class EncryptionUtilsV2IT : EncryptionIT() { @Test fun removeFolder() { - val folder = OCFile("/e/") val enc1 = MockUser("enc1", "Nextcloud") val metadataFile = generateDecryptedFolderMetadataFile(enc1, enc1Cert) assertEquals(2, metadataFile.metadata.files.size) @@ -371,9 +368,7 @@ class EncryptionUtilsV2IT : EncryptionIT() { var updatedMetadata = encryptionUtilsV2.addFolderToMetadata( encryptedFileName, "new subfolder", - metadataFile, - folder, - storageManager + metadataFile ) assertEquals(2, updatedMetadata.metadata.files.size) diff --git a/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java b/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java index 923c9cf99f99..395d82dbe353 100644 --- a/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java @@ -1,6 +1,7 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2020-2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Chris Narkiewicz * SPDX-FileCopyrightText: 2020 Andy Scherzinger @@ -34,7 +35,6 @@ import com.owncloud.android.lib.resources.files.CreateFolderRemoteOperation; import com.owncloud.android.lib.resources.files.ReadFolderRemoteOperation; import com.owncloud.android.lib.resources.files.model.RemoteFile; -import com.owncloud.android.lib.resources.status.E2EVersion; import com.owncloud.android.operations.common.SyncOperation; import com.owncloud.android.utils.EncryptionUtils; import com.owncloud.android.utils.EncryptionUtilsV2; @@ -328,9 +328,7 @@ private RemoteOperationResult encryptedCreateV2(OCFile parent, OwnCloudClient cl // update metadata DecryptedFolderMetadataFile updatedMetadataFile = encryptionUtilsV2.addFolderToMetadata(encryptedFileName, filename, - metadata, - parent, - getStorageManager()); + metadata); // upload metadata encryptionUtilsV2.serializeAndUploadMetadata(parent, @@ -342,6 +340,11 @@ private RemoteOperationResult encryptedCreateV2(OCFile parent, OwnCloudClient cl user, getStorageManager()); + // only persist the new counter locally once the server confirms it, otherwise a concurrent + // folder refresh can see server metadata that looks "older" than the local counter + parent.setE2eCounter(updatedMetadataFile.getMetadata().getCounter()); + getStorageManager().saveFile(parent); + // unlock folder RemoteOperationResult unlockFolderResult = EncryptionUtils.unlockFolder(parent, client, token); diff --git a/app/src/main/java/com/owncloud/android/operations/RemoveRemoteEncryptedFileOperation.kt b/app/src/main/java/com/owncloud/android/operations/RemoveRemoteEncryptedFileOperation.kt index a94178a10bc2..6f391d0384d7 100644 --- a/app/src/main/java/com/owncloud/android/operations/RemoveRemoteEncryptedFileOperation.kt +++ b/app/src/main/java/com/owncloud/android/operations/RemoveRemoteEncryptedFileOperation.kt @@ -183,9 +183,7 @@ class RemoveRemoteEncryptedFileOperation internal constructor( encryptionUtilsV2.removeFileFromMetadata(fileName, metadata) } - parentFolder.setE2eCounter(metadata.metadata.counter) val storageManager = FileDataStorageManager(user, context.contentResolver) - storageManager.saveFile(parentFolder) encryptionUtilsV2.serializeAndUploadMetadata( parentFolder, @@ -198,6 +196,11 @@ class RemoveRemoteEncryptedFileOperation internal constructor( storageManager ) + // only persist the new counter locally once the server confirms it, otherwise a concurrent + // folder refresh can see server metadata that looks "older" than the local counter + parentFolder.setE2eCounter(metadata.metadata.counter) + storageManager.saveFile(parentFolder) + return Pair(result, delete) } 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 bb11eb490dda..8df0ab51968f 100644 --- a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java @@ -1,6 +1,7 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2020-2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2021 Chris Narkiewicz * SPDX-FileCopyrightText: 2017-2018 Andy Scherzinger @@ -907,9 +908,6 @@ private void updateMetadataForV2(DecryptedFolderMetadataFile metadata, Encryptio metadata, getStorageManager()); - parentFile.setE2eCounter(metadata.getMetadata().getCounter()); - getStorageManager().saveFile(parentFile); - // upload metadata encryptionUtilsV2.serializeAndUploadMetadata(parentFile, metadata, @@ -919,6 +917,11 @@ private void updateMetadataForV2(DecryptedFolderMetadataFile metadata, Encryptio mContext, user, getStorageManager()); + + // only persist the new counter locally once the server confirms it, otherwise a concurrent + // folder refresh can see server metadata that looks "older" than the local counter + parentFile.setE2eCounter(metadata.getMetadata().getCounter()); + getStorageManager().saveFile(parentFile); } private void completeE2EUpload(RemoteOperationResult result, E2EFiles e2eFiles, OwnCloudClient client) { diff --git a/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt b/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt index 991d45247465..76af14b0b333 100644 --- a/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt +++ b/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt @@ -1,6 +1,7 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2023 Nextcloud GmbH * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only @@ -12,7 +13,6 @@ import android.content.Context import androidx.annotation.VisibleForTesting import com.google.gson.reflect.TypeToken import com.nextcloud.client.account.User -import com.nextcloud.client.account.UserAccountManagerImpl import com.nextcloud.utils.CmsSignatureVerifier import com.nextcloud.utils.autoRename.AutoRename import com.nextcloud.utils.e2ee.E2EVersionHelper @@ -496,14 +496,10 @@ class EncryptionUtilsV2 { fun addFolderToMetadata( encryptedFileName: String, fileName: String, - metadataFile: DecryptedFolderMetadataFile, - ocFile: OCFile, - fileDataStorageManager: FileDataStorageManager + metadataFile: DecryptedFolderMetadataFile ): DecryptedFolderMetadataFile { metadataFile.metadata.folders[encryptedFileName] = fileName metadataFile.metadata.counter++ - ocFile.setE2eCounter(metadataFile.metadata.counter) - fileDataStorageManager.saveFile(ocFile) return metadataFile } From 43c61f5e33ee13a9145f0257a4f31456155284cf Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 1 Jul 2026 14:36:41 +0200 Subject: [PATCH 2/6] consolidate usages Signed-off-by: alperozturk96 # Conflicts: # app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java --- .../android/utils/EncryptionUtilsV2IT.kt | 3 +- .../datamodel/FileDataStorageManager.java | 13 +- .../operations/CreateFolderOperation.java | 5 +- .../operations/RefreshFolderOperation.java | 25 +- .../RemoveRemoteEncryptedFileOperation.kt | 5 +- .../operations/UploadFileOperation.java | 9 +- .../android/ui/adapter/OCFileListAdapter.java | 4 +- .../fragment/FileDetailSharingFragment.java | 870 ++++++++++++++++++ .../android/utils/EncryptionUtilsV2.kt | 5 +- 9 files changed, 898 insertions(+), 41 deletions(-) create mode 100644 app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java diff --git a/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsV2IT.kt b/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsV2IT.kt index 8d658c5fab7d..ba6ba727f327 100644 --- a/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsV2IT.kt +++ b/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsV2IT.kt @@ -301,8 +301,7 @@ class EncryptionUtilsV2IT : EncryptionIT() { // random string, not real tag EncryptionUtils.generateUid(), EncryptionUtils.generateKey(), - metadataFile, - storageManager + metadataFile ) assertEquals(3, updatedMetadata.metadata.files.size) diff --git a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java index 72777ede4f75..845c946a389d 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java @@ -21,9 +21,9 @@ import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; -import android.media.MediaScannerConnection; import android.content.OperationApplicationException; import android.database.Cursor; +import android.media.MediaScannerConnection; import android.net.Uri; import android.os.RemoteException; import android.provider.MediaStore; @@ -50,8 +50,8 @@ import com.nextcloud.utils.e2ee.E2EVersionHelper; import com.nextcloud.utils.extensions.DateExtensionsKt; import com.nextcloud.utils.extensions.FileExtensionsKt; -import com.nextcloud.utils.extensions.StringExtensionsKt; import com.owncloud.android.MainApp; +import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile; import com.owncloud.android.db.ProviderMeta.ProviderTableMeta; import com.owncloud.android.lib.common.network.WebdavEntry; import com.owncloud.android.lib.common.utils.Log_OC; @@ -2897,4 +2897,13 @@ public FileEntity getFileEntity(OCFile file) { public void updateFileEntity(@NonNull FileEntity entity) { fileDao.update(entity); } + + public void incrementE2ECounter(OCFile file, DecryptedFolderMetadataFile metadata) { + incrementE2ECounter(file, metadata.getMetadata().getCounter()); + } + + public void incrementE2ECounter(OCFile file, long counter) { + file.setE2eCounter(counter); + saveFile(file); + } } diff --git a/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java b/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java index 395d82dbe353..a99cd43963c5 100644 --- a/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java @@ -340,10 +340,7 @@ private RemoteOperationResult encryptedCreateV2(OCFile parent, OwnCloudClient cl user, getStorageManager()); - // only persist the new counter locally once the server confirms it, otherwise a concurrent - // folder refresh can see server metadata that looks "older" than the local counter - parent.setE2eCounter(updatedMetadataFile.getMetadata().getCounter()); - getStorageManager().saveFile(parent); + getStorageManager().incrementE2ECounter(parent, metadata); // unlock folder RemoteOperationResult unlockFolderResult = EncryptionUtils.unlockFolder(parent, client, token); diff --git a/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java b/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java index e47fd552c88e..80dc7b135096 100644 --- a/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java @@ -599,14 +599,10 @@ private void synchronizeData(List folderAndFiles) { FileStorageUtils.searchForLocalFileInDefaultPath(updatedFile, user.getAccountName()); // update file name for encrypted files - if (E2EVersionHelper.INSTANCE.isV1(e2EVersion)) { - updateFileNameForEncryptedFileV1(fileDataStorageManager, - (DecryptedFolderMetadataFileV1) object, - updatedFile); - } else if (object != null) { - updateFileNameForEncryptedFile(fileDataStorageManager, - (DecryptedFolderMetadataFile) object, - updatedFile); + if (E2EVersionHelper.INSTANCE.isV1(e2EVersion) && object instanceof DecryptedFolderMetadataFileV1 metadata) { + updateFileNameForEncryptedFileV1(fileDataStorageManager, metadata, updatedFile); + } else if (object instanceof DecryptedFolderMetadataFile metadata) { + updateFileNameForEncryptedFile(fileDataStorageManager, metadata, updatedFile); if (localFile != null) { updatedFile.setE2eCounter(localFile.getE2eCounter()); } @@ -622,15 +618,12 @@ private void synchronizeData(List folderAndFiles) { // save updated contents in local database // update file name for encrypted files - if (E2EVersionHelper.INSTANCE.isV1(e2EVersion)) { - updateFileNameForEncryptedFileV1(fileDataStorageManager, - (DecryptedFolderMetadataFileV1) object, - mLocalFolder); - } else { - updateFileNameForEncryptedFile(fileDataStorageManager, - (DecryptedFolderMetadataFile) object, - mLocalFolder); + if (E2EVersionHelper.INSTANCE.isV1(e2EVersion) && object instanceof DecryptedFolderMetadataFileV1 metadata) { + updateFileNameForEncryptedFileV1(fileDataStorageManager, metadata, mLocalFolder); + } else if (object instanceof DecryptedFolderMetadataFile metadata){ + updateFileNameForEncryptedFile(fileDataStorageManager, metadata, mLocalFolder); } + fileDataStorageManager.saveFolder(remoteFolder, updatedFiles, localFilesMap.values()); mChildren = updatedFiles; diff --git a/app/src/main/java/com/owncloud/android/operations/RemoveRemoteEncryptedFileOperation.kt b/app/src/main/java/com/owncloud/android/operations/RemoveRemoteEncryptedFileOperation.kt index 6f391d0384d7..a31b5b061f7f 100644 --- a/app/src/main/java/com/owncloud/android/operations/RemoveRemoteEncryptedFileOperation.kt +++ b/app/src/main/java/com/owncloud/android/operations/RemoveRemoteEncryptedFileOperation.kt @@ -196,10 +196,7 @@ class RemoveRemoteEncryptedFileOperation internal constructor( storageManager ) - // only persist the new counter locally once the server confirms it, otherwise a concurrent - // folder refresh can see server metadata that looks "older" than the local counter - parentFolder.setE2eCounter(metadata.metadata.counter) - storageManager.saveFile(parentFolder) + storageManager.incrementE2ECounter(parentFolder, metadata) return Pair(result, delete) } 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 8df0ab51968f..bb9ead8d4faa 100644 --- a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java @@ -54,7 +54,6 @@ import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation; import com.owncloud.android.lib.resources.files.UploadFileRemoteOperation; import com.owncloud.android.lib.resources.files.model.RemoteFile; -import com.owncloud.android.lib.resources.status.E2EVersion; import com.owncloud.android.lib.resources.status.OCCapability; import com.owncloud.android.operations.common.SyncOperation; import com.owncloud.android.operations.e2e.E2EClientData; @@ -905,8 +904,7 @@ private void updateMetadataForV2(DecryptedFolderMetadataFile metadata, Encryptio e2eData.getIv(), e2eData.getEncryptedFile().getAuthenticationTag(), e2eData.getKey(), - metadata, - getStorageManager()); + metadata); // upload metadata encryptionUtilsV2.serializeAndUploadMetadata(parentFile, @@ -918,10 +916,7 @@ private void updateMetadataForV2(DecryptedFolderMetadataFile metadata, Encryptio user, getStorageManager()); - // only persist the new counter locally once the server confirms it, otherwise a concurrent - // folder refresh can see server metadata that looks "older" than the local counter - parentFile.setE2eCounter(metadata.getMetadata().getCounter()); - getStorageManager().saveFile(parentFile); + getStorageManager().incrementE2ECounter(parentFile, metadata); } private void completeE2EUpload(RemoteOperationResult result, E2EFiles e2eFiles, OwnCloudClient client) { diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java index 32950fb06ebe..f71e710de15f 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java @@ -305,8 +305,8 @@ public void updateFileEncryptionById(String fileId, boolean encrypted) { e2eCounter = EncryptionUtils.E2E_V2_INITIAL_COUNTER; } - file.setE2eCounter(e2eCounter); - mStorageManager.saveFile(file); + mStorageManager.incrementE2ECounter(file, e2eCounter); + int position = getItemPosition(file); if (position != -1) { notifyItemChanged(position); 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 new file mode 100644 index 000000000000..7459bef61b23 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java @@ -0,0 +1,870 @@ +/* + * Nextcloud Android client application + * + * @author Andy Scherzinger + * @author Chris Narkiewicz + * @author TSI-mc + * + * Copyright (C) 2018 Andy Scherzinger + * Copyright (C) 2020 Chris Narkiewicz + * Copyright (C) 2023 TSI-mc + * + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ + +package com.owncloud.android.ui.fragment; + +import android.Manifest; +import android.accounts.AccountManager; +import android.app.Activity; +import android.app.SearchManager; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Bundle; +import android.provider.ContactsContract; +import android.text.InputType; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.LinearLayout; + +import com.nextcloud.android.common.ui.theme.utils.ColorRole; +import com.nextcloud.client.account.User; +import com.nextcloud.client.account.UserAccountManager; +import com.nextcloud.client.database.entity.FileEntity; +import com.nextcloud.client.di.Injectable; +import com.nextcloud.client.network.ClientFactory; +import com.nextcloud.client.utils.IntentUtil; +import com.nextcloud.utils.extensions.BundleExtensionsKt; +import com.nextcloud.utils.extensions.OCShareExtensionsKt; +import com.nextcloud.utils.extensions.ViewExtensionsKt; +import com.nextcloud.utils.mdm.MDMConfig; +import com.owncloud.android.R; +import com.owncloud.android.databinding.FileDetailsSharingFragmentBinding; +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.providers.UsersAndGroupsSearchConfig; +import com.owncloud.android.ui.activity.FileActivity; +import com.owncloud.android.ui.activity.FileDisplayActivity; +import com.owncloud.android.ui.adapter.ShareeListAdapter; +import com.owncloud.android.ui.adapter.ShareeListAdapterListener; +import com.owncloud.android.ui.asynctasks.RetrieveHoverCardAsyncTask; +import com.owncloud.android.ui.dialog.SharePasswordDialogFragment; +import com.owncloud.android.ui.fragment.share.RemoteShareRepository; +import com.owncloud.android.ui.fragment.share.ShareRepository; +import com.owncloud.android.ui.fragment.util.FileDetailSharingFragmentHelper; +import com.owncloud.android.ui.helpers.FileOperationsHelper; +import com.owncloud.android.utils.ClipboardUtil; +import com.owncloud.android.utils.DisplayUtils; +import com.owncloud.android.utils.PermissionUtil; +import com.owncloud.android.utils.theme.ViewThemeUtils; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.widget.SearchView; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import kotlin.Unit; + +public class FileDetailSharingFragment extends Fragment implements ShareeListAdapterListener, + DisplayUtils.AvatarGenerationListener, + Injectable, FileDetailsSharingMenuBottomSheetActions, QuickSharingPermissionsBottomSheetDialog.QuickPermissionSharingBottomSheetActions { + + private static final String TAG = "FileDetailSharingFragment"; + private static final String ARG_FILE = "FILE"; + private static final String ARG_USER = "USER"; + + private OCFile file; + private User user; + private OCCapability capabilities; + + private FileOperationsHelper fileOperationsHelper; + private FileActivity fileActivity; + private FileDataStorageManager fileDataStorageManager; + + private FileDetailsSharingFragmentBinding binding; + + private OnEditShareListener onEditShareListener; + + private ShareeListAdapter internalShareeListAdapter; + + private ShareeListAdapter externalShareeListAdapter; + + @Inject UserAccountManager accountManager; + @Inject ClientFactory clientFactory; + @Inject ViewThemeUtils viewThemeUtils; + @Inject UsersAndGroupsSearchConfig searchConfig; + + public static FileDetailSharingFragment newInstance(OCFile file, User user) { + FileDetailSharingFragment fragment = new FileDetailSharingFragment(); + Bundle args = new Bundle(); + args.putParcelable(ARG_FILE, file); + args.putParcelable(ARG_USER, user); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (savedInstanceState != null) { + file = BundleExtensionsKt.getParcelableArgument(savedInstanceState, ARG_FILE, OCFile.class); + user = BundleExtensionsKt.getParcelableArgument(savedInstanceState, ARG_USER, User.class); + } else { + Bundle arguments = getArguments(); + + if (arguments != null) { + file = BundleExtensionsKt.getParcelableArgument(arguments, ARG_FILE, OCFile.class); + user = BundleExtensionsKt.getParcelableArgument(arguments, ARG_USER, User.class); + } + } + + if (file == null) { + throw new IllegalArgumentException("File may not be null"); + } + + if (user == null) { + throw new IllegalArgumentException("Account may not be null"); + } + + fileActivity = (FileActivity) getActivity(); + + if (fileActivity == null) { + throw new IllegalArgumentException("FileActivity may not be null"); + } + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + if (fileActivity == null) { + return; + } + + fileDataStorageManager = fileActivity.getStorageManager(); + fileOperationsHelper = fileActivity.getFileOperationsHelper(); + + // start animation before loading process + final Animation blinkAnimation = AnimationUtils.loadAnimation(requireContext(), R.anim.blink); + binding.shimmerLayout.getRoot().startAnimation(blinkAnimation); + + AccountManager accountManager = AccountManager.get(requireContext()); + String userId = accountManager.getUserData(user.toPlatformAccount(), + com.owncloud.android.lib.common.accounts.AccountUtils.Constants.KEY_USER_ID); + + // internal shares + internalShareeListAdapter = new ShareeListAdapter(fileActivity, + new ArrayList<>(), + this, + userId, + user, + viewThemeUtils, + file.isEncrypted(), + SharesType.INTERNAL); + internalShareeListAdapter.setHasStableIds(true); + binding.sharesListInternal.setAdapter(internalShareeListAdapter); + binding.sharesListInternal.setLayoutManager(new LinearLayoutManager(requireContext())); + + // external shares + externalShareeListAdapter = new ShareeListAdapter(fileActivity, + new ArrayList<>(), + this, + userId, + user, + viewThemeUtils, + file.isEncrypted(), + SharesType.EXTERNAL); + externalShareeListAdapter.setHasStableIds(true); + binding.sharesListExternal.setAdapter(externalShareeListAdapter); + binding.sharesListExternal.setLayoutManager(new LinearLayoutManager(requireContext())); + binding.pickContactEmailBtn.setOnClickListener(v -> checkContactPermission()); + + // start loading process + fetchSharees(); + + setupView(); + } + + private void fetchSharees() { + final var activity = fileActivity; + if (activity == null) { + return; + } + + final var clientRepository = activity.getClientRepository(); + if (clientRepository == null) { + return; + } + + final var storageManager = fileDataStorageManager; + if (storageManager == null) { + return; + } + + ShareRepository shareRepository = new RemoteShareRepository(clientRepository, activity, storageManager); + shareRepository.fetchSharees(file.getRemotePath(), () -> { + if (binding == null) { + return Unit.INSTANCE; + } + + refreshCapabilitiesFromDB(); + refreshSharesFromDB(); + stopLoadingAnimationAndShowShareContainer(); + return Unit.INSTANCE; + }, () -> { + if (binding == null) { + return Unit.INSTANCE; + } + + stopLoadingAnimationAndShowShareContainer(); + DisplayUtils.showSnackMessage(this, R.string.error_fetching_sharees); + return Unit.INSTANCE; + }); + } + + // stop loading animation + private void stopLoadingAnimationAndShowShareContainer() { + if (binding == null) { + return; + } + + final LinearLayout shimmerLayout = binding.shimmerLayout.getRoot(); + shimmerLayout.clearAnimation(); + shimmerLayout.setVisibility(View.GONE); + + binding.shareContainer.setVisibility(View.VISIBLE); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + binding = FileDetailsSharingFragmentBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + if (!(getActivity() instanceof FileActivity)) { + throw new IllegalArgumentException("Calling activity must be of type FileActivity"); + } + + try { + onEditShareListener = (OnEditShareListener) context; + } catch (Exception e) { + throw new IllegalArgumentException("Calling activity must implement the interface" + e); + } + } + + @Override + public void onStart() { + super.onStart(); + searchConfig.setSearchOnlyUsers(file.isEncrypted()); + } + + @Override + public void onStop() { + super.onStop(); + searchConfig.reset(); + } + + private void resetSearchView() { + toggleSearchViewEnable(binding.searchView, true); + binding.searchView.setInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + binding.searchView.setQueryHint(null); + binding.searchView.setQuery("", false); + binding.pickContactEmailBtn.setVisibility(View.VISIBLE); + } + + private void setupView() { + resetSearchView(); + setShareWithYou(); + + OCFile parentFile = fileDataStorageManager.getFileById(file.getParentId()); + + FileDetailSharingFragmentHelper.setupSearchView( + (SearchManager) fileActivity.getSystemService(Context.SEARCH_SERVICE), + binding.searchView, + fileActivity.getComponentName()); + viewThemeUtils.material.themeSearchCardView(binding.searchCardWrapper); + viewThemeUtils.files.themeContentSearchView(binding.searchView); + viewThemeUtils.platform.colorImageView(binding.searchViewIcon, ColorRole.ON_SURFACE_VARIANT); + viewThemeUtils.platform.colorImageView(binding.pickContactEmailBtn, ColorRole.ON_SURFACE_VARIANT); + + viewThemeUtils.material.colorMaterialButtonPrimaryOutlined(binding.sendCopyBtn); + + viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(binding.sharesListInternalShowAll); + viewThemeUtils.material.colorMaterialTextButton(binding.sharesListInternalShowAll); + binding.sharesListInternalShowAll.setOnClickListener(view -> { + internalShareeListAdapter.toggleShowAll(); + int textRes = internalShareeListAdapter.isShowAll() ? R.string.show_less : R.string.show_all; + binding.sharesListInternalShowAll.setText(textRes); + }); + + viewThemeUtils.material.colorMaterialButtonPrimaryOutlined(binding.createLink); + + viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(binding.sharesListExternalShowAll); + viewThemeUtils.material.colorMaterialTextButton(binding.sharesListExternalShowAll); + binding.sharesListExternalShowAll.setOnClickListener(view -> { + externalShareeListAdapter.toggleShowAll(); + int textRes = externalShareeListAdapter.isShowAll() ? R.string.show_less : R.string.show_all; + binding.sharesListExternalShowAll.setText(textRes); + }); + + if (file.canReshare() && !FileDetailSharingFragmentHelper.isPublicShareDisabled(capabilities)) { + if (file.isEncrypted() || (parentFile != null && parentFile.isEncrypted())) { + 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); + } + } + }); + } else { + binding.createLink.setText(R.string.create_link); + binding.searchView.setQueryHint(getResources().getString(R.string.share_search_internal)); + } + + binding.createLink.setOnClickListener(v -> createPublicShareLink()); + + } else { + binding.searchView.setQueryHint(getResources().getString(R.string.resharing_is_not_allowed)); + binding.createLink.setVisibility(View.GONE); + binding.externalSharesHeadline.setVisibility(View.GONE); + binding.searchView.setInputType(InputType.TYPE_NULL); + binding.pickContactEmailBtn.setVisibility(View.GONE); + toggleSearchViewEnable(binding.searchView, false); + binding.createLink.setOnClickListener(null); + } + + checkShareViaUser(); + + if (file.isFolder()) { + binding.sendCopyBtn.setVisibility(View.GONE); + } + binding.sendCopyBtn.setOnClickListener(v -> + startActivity(Intent.createChooser(IntentUtil.createSendIntent(requireContext(), file), + requireContext().getString(R.string.activity_chooser_send_file_title))) + ); + } + + 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) { + fileDataStorageManager.incrementE2ECounter(file, decryptedMetadata); + } + } 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); + } + } + + private void toggleSearchViewEnable(View view, boolean enable) { + view.setEnabled(enable); + if (view instanceof ViewGroup viewGroup) { + for (int i = 0; i < viewGroup.getChildCount(); i++) { + toggleSearchViewEnable(viewGroup.getChildAt(i), enable); + } + } + } + + private void setShareWithYou() { + if (accountManager.userOwnsFile(file, user)) { + binding.sharedWithYouContainer.setVisibility(View.GONE); + } else { + binding.sharedWithYouUsername.setText( + String.format(getString(R.string.shared_with_you_by), file.getOwnerDisplayName())); + DisplayUtils.setAvatar(user, + file.getOwnerId(), + this, + getResources().getDimension( + R.dimen.file_list_item_avatar_icon_radius), + getResources(), + binding.sharedWithYouAvatar, + getContext()); + binding.sharedWithYouAvatar.setVisibility(View.VISIBLE); + + String note = file.getNote(); + + if (!TextUtils.isEmpty(note)) { + binding.sharedWithYouNote.setText(file.getNote()); + binding.sharedWithYouNoteContainer.setVisibility(View.VISIBLE); + } else { + binding.sharedWithYouNoteContainer.setVisibility(View.GONE); + } + } + } + + @Override + public void copyInternalLink() { + OwnCloudAccount account = accountManager.getCurrentOwnCloudAccount(); + + if (account == null) { + DisplayUtils.showSnackMessage(this, R.string.could_not_retrieve_url); + return; + } + + FileDisplayActivity.showShareLinkDialog(fileActivity, file, createInternalLink(account, file)); + } + + private String createInternalLink(OwnCloudAccount account, OCFile file) { + return account.getBaseUri() + "/index.php/f/" + file.getLocalId(); + } + + @Override + public void createPublicShareLink() { + if (capabilities != null && (capabilities.getFilesSharingPublicPasswordEnforced().isTrue() || + capabilities.getFilesSharingPublicAskForOptionalPassword().isTrue())) { + // password enforced by server, request to the user before trying to create + requestPasswordForShareViaLink(true, + capabilities.getFilesSharingPublicAskForOptionalPassword().isTrue()); + + } else { + // create without password if not enforced by server or we don't know if enforced; + fileOperationsHelper.shareFileViaPublicShare(file, null); + } + } + + @Override + public void createSecureFileDrop() { + fileOperationsHelper.shareFolderViaSecureFileDrop(file); + } + + private void showSendLinkTo(OCShare publicShare) { + if (file.isSharedViaLink()) { + if (TextUtils.isEmpty(publicShare.getShareLink())) { + fileOperationsHelper.getFileWithLink(file, viewThemeUtils); + } else { + FileDisplayActivity.showShareLinkDialog(fileActivity, file, publicShare.getShareLink()); + } + } + } + + public void copyLink(OCShare share) { + if (file.isSharedViaLink()) { + if (TextUtils.isEmpty(share.getShareLink())) { + fileOperationsHelper.getFileWithLink(file, viewThemeUtils); + } else { + ClipboardUtil.copyToClipboard(requireActivity(), share.getShareLink()); + } + } + } + + /** + * show share action bottom sheet + * + * @param share + */ + @Override + @VisibleForTesting + public void showSharingMenuActionSheet(OCShare share) { + if (fileActivity != null && !fileActivity.isFinishing()) { + new FileDetailSharingMenuBottomSheetDialog(fileActivity, this, share, viewThemeUtils, file.isEncrypted()).show(); + } + } + + /** + * show quick sharing permission dialog + * + * @param share + */ + @Override + public void showPermissionsDialog(OCShare share) { + new QuickSharingPermissionsBottomSheetDialog(fileActivity, this, share, viewThemeUtils, file.isEncrypted()).show(); + } + + /** + * Updates the UI after the result of an update operation on the edited {@link OCFile}. + * + * @param result {@link RemoteOperationResult} of an update on the edited {@link OCFile} sharing information. + * @param file the edited {@link OCFile} + * @see #onUpdateShareInformation(RemoteOperationResult) + */ + public void onUpdateShareInformation(RemoteOperationResult result, OCFile file) { + this.file = file; + + onUpdateShareInformation(result); + } + + /** + * Updates the UI after the result of an update operation on the edited {@link OCFile}. Keeps the current {@link + * OCFile held by this fragment}. + * + * @param result {@link RemoteOperationResult} of an update on the edited {@link OCFile} sharing information. + * @see #onUpdateShareInformation(RemoteOperationResult, OCFile) + */ + public void onUpdateShareInformation(RemoteOperationResult result) { + if (binding == null) { + return; + } + + if (result.isSuccess()) { + refreshUiFromDB(); + } else { + setupView(); + } + } + + /** + * Get {@link OCShare} instance from DB and updates the UI. + */ + private void refreshUiFromDB() { + refreshSharesFromDB(); + // Updates UI with new state + setupView(); + } + + private void unShareWith(OCShare share) { + fileOperationsHelper.unShareShare(file, share.getId()); + } + + /** + * Starts a dialog that requests a password to the user to protect a share link. + * + * @param createShare When 'true', the request for password will be followed by the creation of a new public + * link; when 'false', a public share is assumed to exist, and the password is bound to it. + * @param askForPassword if true, password is optional + */ + public void requestPasswordForShareViaLink(boolean createShare, boolean askForPassword) { + SharePasswordDialogFragment dialog = SharePasswordDialogFragment.newInstance(file, + createShare, + askForPassword); + dialog.show(getChildFragmentManager(), SharePasswordDialogFragment.PASSWORD_FRAGMENT); + } + + @Override + public void requestPasswordForShare(OCShare share, boolean askForPassword) { + SharePasswordDialogFragment dialog = SharePasswordDialogFragment.newInstance(share, askForPassword); + dialog.show(getChildFragmentManager(), SharePasswordDialogFragment.PASSWORD_FRAGMENT); + } + + @Override + public void showProfileBottomSheet(User user, String shareWith) { + if (user.getServer().getVersion().isNewerOrEqual(NextcloudVersion.nextcloud_23)) { + new RetrieveHoverCardAsyncTask(user, + shareWith, + fileActivity, + clientFactory, + viewThemeUtils).execute(); + } + } + + public void refreshCapabilitiesFromDB() { + capabilities = fileDataStorageManager.getCapability(user.getAccountName()); + } + + /** + * Get public link from the DB to fill in the "Share link" section in the UI. Takes into account server capabilities + * before reading database. + */ + @SuppressFBWarnings("PSC") + public void refreshSharesFromDB() { + if (binding == null) { + return; + } + + OCFile newFile = fileDataStorageManager.getFileById(file.getFileId()); + if (newFile != null) { + file = newFile; + } + + if (internalShareeListAdapter == null) { + DisplayUtils.showSnackMessage(this, R.string.could_not_retrieve_shares); + return; + } + + internalShareeListAdapter.removeAll(); + + // to show share with users/groups info + List shares = fileDataStorageManager.getSharesWithForAFile(file.getRemotePath(), + user.getAccountName()); + + List internalShares = new ArrayList<>(); + List externalShares = new ArrayList<>(); + + for (OCShare share : shares) { + if (share.getShareType() != null) { + switch (share.getShareType()) { + case PUBLIC_LINK: + case FEDERATED_GROUP: + case FEDERATED: + case EMAIL: + externalShares.add(share); + break; + + default: + internalShares.add(share); + break; + } + } + } + + internalShareeListAdapter.addShares(internalShares); + ViewExtensionsKt.setVisibleIf(binding.sharesListInternalShowAll, internalShareeListAdapter.shares.size() > 3); + + addExternalAndPublicShares(externalShares); + ViewExtensionsKt.setVisibleIf(binding.sharesListExternalShowAll, externalShareeListAdapter.shares.size() > 3); + } + + private void addExternalAndPublicShares(List externalShares) { + final var publicShares = fileDataStorageManager.getSharesByPathAndType(file.getRemotePath(), ShareType.PUBLIC_LINK, ""); + externalShareeListAdapter.removeAll(); + final var shares = OCShareExtensionsKt.mergeDistinctByToken(externalShares, publicShares); + externalShareeListAdapter.addShares(shares); + } + + private void checkContactPermission() { + if (PermissionUtil.checkSelfPermission(requireActivity(), Manifest.permission.READ_CONTACTS)) { + pickContactEmail(); + } else { + requestContactPermissionLauncher.launch(Manifest.permission.READ_CONTACTS); + } + } + + private void pickContactEmail() { + Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Email.CONTENT_URI); + + if (intent.resolveActivity(requireContext().getPackageManager()) != null) { + onContactSelectionResultLauncher.launch(intent); + } else { + DisplayUtils.showSnackMessage(this, R.string.file_detail_sharing_fragment_no_contact_app_message); + } + } + + private void handleContactResult(@NonNull Uri contactUri) { + // Define the projection to get all email addresses. + String[] projection = {ContactsContract.CommonDataKinds.Email.ADDRESS}; + + Cursor cursor = fileActivity.getContentResolver().query(contactUri, projection, null, null, null); + + if (cursor != null) { + if (cursor.moveToFirst()) { + // The contact has only one email address, use it. + int columnIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.ADDRESS); + if (columnIndex != -1) { + // Use the email address as needed. + // email variable contains the selected contact's email address. + String email = cursor.getString(columnIndex); + binding.searchView.post(() -> { + if (binding == null) { + return; + } + + binding.searchView.setQuery(email, false); + binding.searchView.requestFocus(); + }); + } else { + DisplayUtils.showSnackMessage(this, R.string.email_pick_failed); + Log_OC.e(FileDetailSharingFragment.class.getSimpleName(), "Failed to pick email address."); + } + } else { + DisplayUtils.showSnackMessage(this, R.string.email_pick_failed); + Log_OC.e(FileDetailSharingFragment.class.getSimpleName(), "Failed to pick email address as no Email found."); + } + cursor.close(); + } else { + DisplayUtils.showSnackMessage(this, R.string.email_pick_failed); + Log_OC.e(FileDetailSharingFragment.class.getSimpleName(), "Failed to pick email address as Cursor is null."); + } + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + outState.putParcelable(ARG_FILE, file); + outState.putParcelable(ARG_USER, user); + } + + @Override + public void avatarGenerated(Drawable avatarDrawable, Object callContext) { + if (binding == null) { + return; + } + binding.sharedWithYouAvatar.setImageDrawable(avatarDrawable); + } + + @Override + public boolean shouldCallGeneratedCallback(String tag, Object callContext) { + return false; + } + + private boolean isReshareForbidden(OCShare share) { + return ShareType.FEDERATED == share.getShareType() || + capabilities != null && capabilities.getFilesSharingResharing().isFalse(); + } + + @VisibleForTesting + public void search(String query) { + SearchView searchView = requireView().findViewById(R.id.searchView); + searchView.setQuery(query, true); + } + + @Override + public void advancedPermissions(OCShare share) { + modifyExistingShare(share, FileDetailsSharingProcessFragment.SCREEN_TYPE_PERMISSION); + } + + @Override + public void sendNewEmail(OCShare share) { + modifyExistingShare(share, FileDetailsSharingProcessFragment.SCREEN_TYPE_NOTE); + } + + @Override + public void unShare(OCShare share) { + if (binding == null) { + return; + } + + unShareWith(share); + + FileEntity entity = fileDataStorageManager.getFileEntity(file); + + if (binding.sharesListInternal.getAdapter() instanceof ShareeListAdapter adapter) { + adapter.remove(share); + if (entity != null && adapter.isAdapterEmpty()) { + entity.setSharedWithSharee(0); + fileDataStorageManager.updateFileEntity(entity); + } + } else if (binding.sharesListExternal.getAdapter() instanceof ShareeListAdapter adapter) { + adapter.remove(share); + if (entity != null && adapter.isAdapterEmpty()) { + entity.setSharedViaLink(0); + fileDataStorageManager.updateFileEntity(entity); + } + } else { + DisplayUtils.showSnackMessage(this, R.string.failed_update_ui); + } + } + + @Override + public void sendLink(OCShare share) { + if (file.isSharedViaLink() && !TextUtils.isEmpty(share.getShareLink())) { + FileDisplayActivity.showShareLinkDialog(fileActivity, file, share.getShareLink()); + } else { + showSendLinkTo(share); + } + } + + @Override + public void addAnotherLink(OCShare share) { + createPublicShareLink(); + } + + private void modifyExistingShare(OCShare share, int screenTypePermission) { + onEditShareListener.editExistingShare(share, screenTypePermission, !isReshareForbidden(share)); + } + + @Override + public void onQuickPermissionChanged(OCShare share, int permission) { + fileOperationsHelper.setPermissionsToShare(share, permission); + } + + @Override + public void openShareDetailWithCustomPermissions(OCShare share) { + modifyExistingShare(share, FileDetailsSharingProcessFragment.SCREEN_TYPE_PERMISSION_WITH_CUSTOM_PERMISSION); + } + + //launcher for contact permission + private final ActivityResultLauncher requestContactPermissionLauncher = + registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> { + if (isGranted) { + pickContactEmail(); + } else { + DisplayUtils.showSnackMessage(this, R.string.contact_no_permission); + } + }); + + //launcher to handle contact selection + private final ActivityResultLauncher onContactSelectionResultLauncher = + registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == Activity.RESULT_OK) { + Intent intent = result.getData(); + if (intent == null) { + DisplayUtils.showSnackMessage(this, R.string.email_pick_failed); + return; + } + + Uri contactUri = intent.getData(); + if (contactUri == null) { + DisplayUtils.showSnackMessage(this, R.string.email_pick_failed); + return; + } + + handleContactResult(contactUri); + + } + }); + + public interface OnEditShareListener { + void editExistingShare(OCShare share, int screenTypePermission, boolean isReshareShown); + + void onShareProcessClosed(); + } +} diff --git a/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt b/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt index 76af14b0b333..2b3a31ab0eea 100644 --- a/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt +++ b/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt @@ -474,8 +474,7 @@ class EncryptionUtilsV2 { initializationVector: ByteArray, authenticationTag: String, key: ByteArray, - metadataFile: DecryptedFolderMetadataFile, - fileDataStorageManager: FileDataStorageManager + metadataFile: DecryptedFolderMetadataFile ): DecryptedFolderMetadataFile { val decryptedFile = DecryptedFile( ocFile.decryptedFileName, @@ -487,8 +486,6 @@ class EncryptionUtilsV2 { metadataFile.metadata.files[encryptedFileName] = decryptedFile metadataFile.metadata.counter++ - ocFile.setE2eCounter(metadataFile.metadata.counter) - fileDataStorageManager.saveFile(ocFile) return metadataFile } From 1037ad1409cd018e7880cf31524ed93dd21bae6b Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 1 Jul 2026 14:47:24 +0200 Subject: [PATCH 3/6] improve naming Signed-off-by: alperozturk96 --- .../owncloud/android/datamodel/FileDataStorageManager.java | 6 +++--- .../owncloud/android/operations/CreateFolderOperation.java | 2 +- .../owncloud/android/operations/RefreshFolderOperation.java | 2 +- .../operations/RemoveRemoteEncryptedFileOperation.kt | 2 +- .../owncloud/android/operations/UploadFileOperation.java | 2 +- .../com/owncloud/android/ui/adapter/OCFileListAdapter.java | 2 +- .../android/ui/fragment/FileDetailSharingFragment.java | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java index 845c946a389d..64b045734a00 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java @@ -2898,11 +2898,11 @@ public void updateFileEntity(@NonNull FileEntity entity) { fileDao.update(entity); } - public void incrementE2ECounter(OCFile file, DecryptedFolderMetadataFile metadata) { - incrementE2ECounter(file, metadata.getMetadata().getCounter()); + public void updateE2EECounter(OCFile file, DecryptedFolderMetadataFile metadata) { + updateE2EECounter(file, metadata.getMetadata().getCounter()); } - public void incrementE2ECounter(OCFile file, long counter) { + public void updateE2EECounter(OCFile file, long counter) { file.setE2eCounter(counter); saveFile(file); } diff --git a/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java b/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java index a99cd43963c5..1afc6d45091a 100644 --- a/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java @@ -340,7 +340,7 @@ private RemoteOperationResult encryptedCreateV2(OCFile parent, OwnCloudClient cl user, getStorageManager()); - getStorageManager().incrementE2ECounter(parent, metadata); + getStorageManager().updateE2EECounter(parent, metadata); // unlock folder RemoteOperationResult unlockFolderResult = EncryptionUtils.unlockFolder(parent, client, token); diff --git a/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java b/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java index 80dc7b135096..493c13358682 100644 --- a/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java @@ -620,7 +620,7 @@ private void synchronizeData(List folderAndFiles) { // update file name for encrypted files if (E2EVersionHelper.INSTANCE.isV1(e2EVersion) && object instanceof DecryptedFolderMetadataFileV1 metadata) { updateFileNameForEncryptedFileV1(fileDataStorageManager, metadata, mLocalFolder); - } else if (object instanceof DecryptedFolderMetadataFile metadata){ + } else if (object instanceof DecryptedFolderMetadataFile metadata) { updateFileNameForEncryptedFile(fileDataStorageManager, metadata, mLocalFolder); } diff --git a/app/src/main/java/com/owncloud/android/operations/RemoveRemoteEncryptedFileOperation.kt b/app/src/main/java/com/owncloud/android/operations/RemoveRemoteEncryptedFileOperation.kt index a31b5b061f7f..65020e6eee08 100644 --- a/app/src/main/java/com/owncloud/android/operations/RemoveRemoteEncryptedFileOperation.kt +++ b/app/src/main/java/com/owncloud/android/operations/RemoveRemoteEncryptedFileOperation.kt @@ -196,7 +196,7 @@ class RemoveRemoteEncryptedFileOperation internal constructor( storageManager ) - storageManager.incrementE2ECounter(parentFolder, metadata) + storageManager.updateE2EECounter(parentFolder, metadata) return Pair(result, delete) } 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 bb9ead8d4faa..54999779570f 100644 --- a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java @@ -916,7 +916,7 @@ private void updateMetadataForV2(DecryptedFolderMetadataFile metadata, Encryptio user, getStorageManager()); - getStorageManager().incrementE2ECounter(parentFile, metadata); + getStorageManager().updateE2EECounter(parentFile, metadata); } private void completeE2EUpload(RemoteOperationResult result, E2EFiles e2eFiles, OwnCloudClient client) { diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java index f71e710de15f..352bc6472b8d 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java @@ -305,7 +305,7 @@ public void updateFileEncryptionById(String fileId, boolean encrypted) { e2eCounter = EncryptionUtils.E2E_V2_INITIAL_COUNTER; } - mStorageManager.incrementE2ECounter(file, e2eCounter); + mStorageManager.updateE2EECounter(file, e2eCounter); int position = getItemPosition(file); if (position != -1) { 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 7459bef61b23..46cd70671de3 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 @@ -406,7 +406,7 @@ private void fetchE2EECounter(Runnable onComplete) { OwnCloudClient client = clientFactory.create(user); Object metadata = RefreshFolderOperation.getDecryptedFolderMetadata(true, file, client, user, context); if (metadata instanceof DecryptedFolderMetadataFile decryptedMetadata) { - fileDataStorageManager.incrementE2ECounter(file, decryptedMetadata); + fileDataStorageManager.updateE2EECounter(file, decryptedMetadata); } } catch (Exception e) { Log_OC.e(TAG, "Error refreshing E2E counter: " + e.getMessage()); From b33d8bca4aa710b3be2485f557a449f965012343 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 1 Jul 2026 15:04:44 +0200 Subject: [PATCH 4/6] wait until current uploads finish then verify metadata Signed-off-by: alperozturk96 --- .../operations/RefreshFolderOperation.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java b/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java index 493c13358682..660045f443cc 100644 --- a/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java @@ -15,9 +15,11 @@ import com.google.gson.Gson; import com.nextcloud.android.lib.resources.directediting.DirectEditingObtainRemoteOperation; import com.nextcloud.client.account.User; +import com.nextcloud.client.jobs.BackgroundJobManagerImpl; import com.nextcloud.common.NextcloudClient; import com.nextcloud.utils.e2ee.E2EVersionHelper; import com.nextcloud.utils.extensions.StringExtensionsKt; +import com.nextcloud.utils.extensions.WorkManagerExtensionsKt; import com.owncloud.android.datamodel.ArbitraryDataProvider; import com.owncloud.android.datamodel.ArbitraryDataProviderImpl; import com.owncloud.android.datamodel.FileDataStorageManager; @@ -57,6 +59,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import androidx.work.WorkManager; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import static com.owncloud.android.datamodel.OCFile.PATH_SEPARATOR; @@ -532,6 +535,24 @@ private void synchronizeData(List folderAndFiles) { Object object = null; if (mLocalFolder.isEncrypted()) { + final var workManager = WorkManager.getInstance(mContext); + final var tag = BackgroundJobManagerImpl.JOB_FILES_UPLOAD + user.getAccountName(); + boolean isWorkScheduled = WorkManagerExtensionsKt.isWorkScheduled(workManager, tag); + + Log_OC.d(TAG, "file upload worker status: " + isWorkScheduled); + + // wait until current uploads finish then verify metadata + while (isWorkScheduled) { + try { + Thread.sleep(500); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + isWorkScheduled = WorkManagerExtensionsKt.isWorkScheduled(workManager, tag); + Log_OC.d(TAG, "file upload worker updated status: " + isWorkScheduled); + } + object = getDecryptedFolderMetadata(encryptedAncestor, mLocalFolder, getClient(), From 9be910ead5fc1ddae7398e0cab09b14c42fc9d7d Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 9 Jul 2026 10:16:18 +0200 Subject: [PATCH 5/6] wip Signed-off-by: alperozturk96 --- .../fragment/FileDetailSharingFragment.java | 870 ------------------ .../ui/fragment/FileDetailSharingFragment.kt | 3 +- 2 files changed, 1 insertion(+), 872 deletions(-) delete mode 100644 app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java 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 deleted file mode 100644 index 46cd70671de3..000000000000 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java +++ /dev/null @@ -1,870 +0,0 @@ -/* - * Nextcloud Android client application - * - * @author Andy Scherzinger - * @author Chris Narkiewicz - * @author TSI-mc - * - * Copyright (C) 2018 Andy Scherzinger - * Copyright (C) 2020 Chris Narkiewicz - * Copyright (C) 2023 TSI-mc - * - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ - -package com.owncloud.android.ui.fragment; - -import android.Manifest; -import android.accounts.AccountManager; -import android.app.Activity; -import android.app.SearchManager; -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.Bundle; -import android.provider.ContactsContract; -import android.text.InputType; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.animation.Animation; -import android.view.animation.AnimationUtils; -import android.widget.LinearLayout; - -import com.nextcloud.android.common.ui.theme.utils.ColorRole; -import com.nextcloud.client.account.User; -import com.nextcloud.client.account.UserAccountManager; -import com.nextcloud.client.database.entity.FileEntity; -import com.nextcloud.client.di.Injectable; -import com.nextcloud.client.network.ClientFactory; -import com.nextcloud.client.utils.IntentUtil; -import com.nextcloud.utils.extensions.BundleExtensionsKt; -import com.nextcloud.utils.extensions.OCShareExtensionsKt; -import com.nextcloud.utils.extensions.ViewExtensionsKt; -import com.nextcloud.utils.mdm.MDMConfig; -import com.owncloud.android.R; -import com.owncloud.android.databinding.FileDetailsSharingFragmentBinding; -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.providers.UsersAndGroupsSearchConfig; -import com.owncloud.android.ui.activity.FileActivity; -import com.owncloud.android.ui.activity.FileDisplayActivity; -import com.owncloud.android.ui.adapter.ShareeListAdapter; -import com.owncloud.android.ui.adapter.ShareeListAdapterListener; -import com.owncloud.android.ui.asynctasks.RetrieveHoverCardAsyncTask; -import com.owncloud.android.ui.dialog.SharePasswordDialogFragment; -import com.owncloud.android.ui.fragment.share.RemoteShareRepository; -import com.owncloud.android.ui.fragment.share.ShareRepository; -import com.owncloud.android.ui.fragment.util.FileDetailSharingFragmentHelper; -import com.owncloud.android.ui.helpers.FileOperationsHelper; -import com.owncloud.android.utils.ClipboardUtil; -import com.owncloud.android.utils.DisplayUtils; -import com.owncloud.android.utils.PermissionUtil; -import com.owncloud.android.utils.theme.ViewThemeUtils; - -import java.util.ArrayList; -import java.util.List; - -import javax.inject.Inject; - -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import androidx.appcompat.widget.SearchView; -import androidx.fragment.app.Fragment; -import androidx.recyclerview.widget.LinearLayoutManager; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import kotlin.Unit; - -public class FileDetailSharingFragment extends Fragment implements ShareeListAdapterListener, - DisplayUtils.AvatarGenerationListener, - Injectable, FileDetailsSharingMenuBottomSheetActions, QuickSharingPermissionsBottomSheetDialog.QuickPermissionSharingBottomSheetActions { - - private static final String TAG = "FileDetailSharingFragment"; - private static final String ARG_FILE = "FILE"; - private static final String ARG_USER = "USER"; - - private OCFile file; - private User user; - private OCCapability capabilities; - - private FileOperationsHelper fileOperationsHelper; - private FileActivity fileActivity; - private FileDataStorageManager fileDataStorageManager; - - private FileDetailsSharingFragmentBinding binding; - - private OnEditShareListener onEditShareListener; - - private ShareeListAdapter internalShareeListAdapter; - - private ShareeListAdapter externalShareeListAdapter; - - @Inject UserAccountManager accountManager; - @Inject ClientFactory clientFactory; - @Inject ViewThemeUtils viewThemeUtils; - @Inject UsersAndGroupsSearchConfig searchConfig; - - public static FileDetailSharingFragment newInstance(OCFile file, User user) { - FileDetailSharingFragment fragment = new FileDetailSharingFragment(); - Bundle args = new Bundle(); - args.putParcelable(ARG_FILE, file); - args.putParcelable(ARG_USER, user); - fragment.setArguments(args); - return fragment; - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - if (savedInstanceState != null) { - file = BundleExtensionsKt.getParcelableArgument(savedInstanceState, ARG_FILE, OCFile.class); - user = BundleExtensionsKt.getParcelableArgument(savedInstanceState, ARG_USER, User.class); - } else { - Bundle arguments = getArguments(); - - if (arguments != null) { - file = BundleExtensionsKt.getParcelableArgument(arguments, ARG_FILE, OCFile.class); - user = BundleExtensionsKt.getParcelableArgument(arguments, ARG_USER, User.class); - } - } - - if (file == null) { - throw new IllegalArgumentException("File may not be null"); - } - - if (user == null) { - throw new IllegalArgumentException("Account may not be null"); - } - - fileActivity = (FileActivity) getActivity(); - - if (fileActivity == null) { - throw new IllegalArgumentException("FileActivity may not be null"); - } - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - if (fileActivity == null) { - return; - } - - fileDataStorageManager = fileActivity.getStorageManager(); - fileOperationsHelper = fileActivity.getFileOperationsHelper(); - - // start animation before loading process - final Animation blinkAnimation = AnimationUtils.loadAnimation(requireContext(), R.anim.blink); - binding.shimmerLayout.getRoot().startAnimation(blinkAnimation); - - AccountManager accountManager = AccountManager.get(requireContext()); - String userId = accountManager.getUserData(user.toPlatformAccount(), - com.owncloud.android.lib.common.accounts.AccountUtils.Constants.KEY_USER_ID); - - // internal shares - internalShareeListAdapter = new ShareeListAdapter(fileActivity, - new ArrayList<>(), - this, - userId, - user, - viewThemeUtils, - file.isEncrypted(), - SharesType.INTERNAL); - internalShareeListAdapter.setHasStableIds(true); - binding.sharesListInternal.setAdapter(internalShareeListAdapter); - binding.sharesListInternal.setLayoutManager(new LinearLayoutManager(requireContext())); - - // external shares - externalShareeListAdapter = new ShareeListAdapter(fileActivity, - new ArrayList<>(), - this, - userId, - user, - viewThemeUtils, - file.isEncrypted(), - SharesType.EXTERNAL); - externalShareeListAdapter.setHasStableIds(true); - binding.sharesListExternal.setAdapter(externalShareeListAdapter); - binding.sharesListExternal.setLayoutManager(new LinearLayoutManager(requireContext())); - binding.pickContactEmailBtn.setOnClickListener(v -> checkContactPermission()); - - // start loading process - fetchSharees(); - - setupView(); - } - - private void fetchSharees() { - final var activity = fileActivity; - if (activity == null) { - return; - } - - final var clientRepository = activity.getClientRepository(); - if (clientRepository == null) { - return; - } - - final var storageManager = fileDataStorageManager; - if (storageManager == null) { - return; - } - - ShareRepository shareRepository = new RemoteShareRepository(clientRepository, activity, storageManager); - shareRepository.fetchSharees(file.getRemotePath(), () -> { - if (binding == null) { - return Unit.INSTANCE; - } - - refreshCapabilitiesFromDB(); - refreshSharesFromDB(); - stopLoadingAnimationAndShowShareContainer(); - return Unit.INSTANCE; - }, () -> { - if (binding == null) { - return Unit.INSTANCE; - } - - stopLoadingAnimationAndShowShareContainer(); - DisplayUtils.showSnackMessage(this, R.string.error_fetching_sharees); - return Unit.INSTANCE; - }); - } - - // stop loading animation - private void stopLoadingAnimationAndShowShareContainer() { - if (binding == null) { - return; - } - - final LinearLayout shimmerLayout = binding.shimmerLayout.getRoot(); - shimmerLayout.clearAnimation(); - shimmerLayout.setVisibility(View.GONE); - - binding.shareContainer.setVisibility(View.VISIBLE); - } - - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - binding = FileDetailsSharingFragmentBinding.inflate(inflater, container, false); - return binding.getRoot(); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - binding = null; - } - - @Override - public void onAttach(@NonNull Context context) { - super.onAttach(context); - if (!(getActivity() instanceof FileActivity)) { - throw new IllegalArgumentException("Calling activity must be of type FileActivity"); - } - - try { - onEditShareListener = (OnEditShareListener) context; - } catch (Exception e) { - throw new IllegalArgumentException("Calling activity must implement the interface" + e); - } - } - - @Override - public void onStart() { - super.onStart(); - searchConfig.setSearchOnlyUsers(file.isEncrypted()); - } - - @Override - public void onStop() { - super.onStop(); - searchConfig.reset(); - } - - private void resetSearchView() { - toggleSearchViewEnable(binding.searchView, true); - binding.searchView.setInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); - binding.searchView.setQueryHint(null); - binding.searchView.setQuery("", false); - binding.pickContactEmailBtn.setVisibility(View.VISIBLE); - } - - private void setupView() { - resetSearchView(); - setShareWithYou(); - - OCFile parentFile = fileDataStorageManager.getFileById(file.getParentId()); - - FileDetailSharingFragmentHelper.setupSearchView( - (SearchManager) fileActivity.getSystemService(Context.SEARCH_SERVICE), - binding.searchView, - fileActivity.getComponentName()); - viewThemeUtils.material.themeSearchCardView(binding.searchCardWrapper); - viewThemeUtils.files.themeContentSearchView(binding.searchView); - viewThemeUtils.platform.colorImageView(binding.searchViewIcon, ColorRole.ON_SURFACE_VARIANT); - viewThemeUtils.platform.colorImageView(binding.pickContactEmailBtn, ColorRole.ON_SURFACE_VARIANT); - - viewThemeUtils.material.colorMaterialButtonPrimaryOutlined(binding.sendCopyBtn); - - viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(binding.sharesListInternalShowAll); - viewThemeUtils.material.colorMaterialTextButton(binding.sharesListInternalShowAll); - binding.sharesListInternalShowAll.setOnClickListener(view -> { - internalShareeListAdapter.toggleShowAll(); - int textRes = internalShareeListAdapter.isShowAll() ? R.string.show_less : R.string.show_all; - binding.sharesListInternalShowAll.setText(textRes); - }); - - viewThemeUtils.material.colorMaterialButtonPrimaryOutlined(binding.createLink); - - viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(binding.sharesListExternalShowAll); - viewThemeUtils.material.colorMaterialTextButton(binding.sharesListExternalShowAll); - binding.sharesListExternalShowAll.setOnClickListener(view -> { - externalShareeListAdapter.toggleShowAll(); - int textRes = externalShareeListAdapter.isShowAll() ? R.string.show_less : R.string.show_all; - binding.sharesListExternalShowAll.setText(textRes); - }); - - if (file.canReshare() && !FileDetailSharingFragmentHelper.isPublicShareDisabled(capabilities)) { - if (file.isEncrypted() || (parentFile != null && parentFile.isEncrypted())) { - 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); - } - } - }); - } else { - binding.createLink.setText(R.string.create_link); - binding.searchView.setQueryHint(getResources().getString(R.string.share_search_internal)); - } - - binding.createLink.setOnClickListener(v -> createPublicShareLink()); - - } else { - binding.searchView.setQueryHint(getResources().getString(R.string.resharing_is_not_allowed)); - binding.createLink.setVisibility(View.GONE); - binding.externalSharesHeadline.setVisibility(View.GONE); - binding.searchView.setInputType(InputType.TYPE_NULL); - binding.pickContactEmailBtn.setVisibility(View.GONE); - toggleSearchViewEnable(binding.searchView, false); - binding.createLink.setOnClickListener(null); - } - - checkShareViaUser(); - - if (file.isFolder()) { - binding.sendCopyBtn.setVisibility(View.GONE); - } - binding.sendCopyBtn.setOnClickListener(v -> - startActivity(Intent.createChooser(IntentUtil.createSendIntent(requireContext(), file), - requireContext().getString(R.string.activity_chooser_send_file_title))) - ); - } - - 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) { - fileDataStorageManager.updateE2EECounter(file, decryptedMetadata); - } - } 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); - } - } - - private void toggleSearchViewEnable(View view, boolean enable) { - view.setEnabled(enable); - if (view instanceof ViewGroup viewGroup) { - for (int i = 0; i < viewGroup.getChildCount(); i++) { - toggleSearchViewEnable(viewGroup.getChildAt(i), enable); - } - } - } - - private void setShareWithYou() { - if (accountManager.userOwnsFile(file, user)) { - binding.sharedWithYouContainer.setVisibility(View.GONE); - } else { - binding.sharedWithYouUsername.setText( - String.format(getString(R.string.shared_with_you_by), file.getOwnerDisplayName())); - DisplayUtils.setAvatar(user, - file.getOwnerId(), - this, - getResources().getDimension( - R.dimen.file_list_item_avatar_icon_radius), - getResources(), - binding.sharedWithYouAvatar, - getContext()); - binding.sharedWithYouAvatar.setVisibility(View.VISIBLE); - - String note = file.getNote(); - - if (!TextUtils.isEmpty(note)) { - binding.sharedWithYouNote.setText(file.getNote()); - binding.sharedWithYouNoteContainer.setVisibility(View.VISIBLE); - } else { - binding.sharedWithYouNoteContainer.setVisibility(View.GONE); - } - } - } - - @Override - public void copyInternalLink() { - OwnCloudAccount account = accountManager.getCurrentOwnCloudAccount(); - - if (account == null) { - DisplayUtils.showSnackMessage(this, R.string.could_not_retrieve_url); - return; - } - - FileDisplayActivity.showShareLinkDialog(fileActivity, file, createInternalLink(account, file)); - } - - private String createInternalLink(OwnCloudAccount account, OCFile file) { - return account.getBaseUri() + "/index.php/f/" + file.getLocalId(); - } - - @Override - public void createPublicShareLink() { - if (capabilities != null && (capabilities.getFilesSharingPublicPasswordEnforced().isTrue() || - capabilities.getFilesSharingPublicAskForOptionalPassword().isTrue())) { - // password enforced by server, request to the user before trying to create - requestPasswordForShareViaLink(true, - capabilities.getFilesSharingPublicAskForOptionalPassword().isTrue()); - - } else { - // create without password if not enforced by server or we don't know if enforced; - fileOperationsHelper.shareFileViaPublicShare(file, null); - } - } - - @Override - public void createSecureFileDrop() { - fileOperationsHelper.shareFolderViaSecureFileDrop(file); - } - - private void showSendLinkTo(OCShare publicShare) { - if (file.isSharedViaLink()) { - if (TextUtils.isEmpty(publicShare.getShareLink())) { - fileOperationsHelper.getFileWithLink(file, viewThemeUtils); - } else { - FileDisplayActivity.showShareLinkDialog(fileActivity, file, publicShare.getShareLink()); - } - } - } - - public void copyLink(OCShare share) { - if (file.isSharedViaLink()) { - if (TextUtils.isEmpty(share.getShareLink())) { - fileOperationsHelper.getFileWithLink(file, viewThemeUtils); - } else { - ClipboardUtil.copyToClipboard(requireActivity(), share.getShareLink()); - } - } - } - - /** - * show share action bottom sheet - * - * @param share - */ - @Override - @VisibleForTesting - public void showSharingMenuActionSheet(OCShare share) { - if (fileActivity != null && !fileActivity.isFinishing()) { - new FileDetailSharingMenuBottomSheetDialog(fileActivity, this, share, viewThemeUtils, file.isEncrypted()).show(); - } - } - - /** - * show quick sharing permission dialog - * - * @param share - */ - @Override - public void showPermissionsDialog(OCShare share) { - new QuickSharingPermissionsBottomSheetDialog(fileActivity, this, share, viewThemeUtils, file.isEncrypted()).show(); - } - - /** - * Updates the UI after the result of an update operation on the edited {@link OCFile}. - * - * @param result {@link RemoteOperationResult} of an update on the edited {@link OCFile} sharing information. - * @param file the edited {@link OCFile} - * @see #onUpdateShareInformation(RemoteOperationResult) - */ - public void onUpdateShareInformation(RemoteOperationResult result, OCFile file) { - this.file = file; - - onUpdateShareInformation(result); - } - - /** - * Updates the UI after the result of an update operation on the edited {@link OCFile}. Keeps the current {@link - * OCFile held by this fragment}. - * - * @param result {@link RemoteOperationResult} of an update on the edited {@link OCFile} sharing information. - * @see #onUpdateShareInformation(RemoteOperationResult, OCFile) - */ - public void onUpdateShareInformation(RemoteOperationResult result) { - if (binding == null) { - return; - } - - if (result.isSuccess()) { - refreshUiFromDB(); - } else { - setupView(); - } - } - - /** - * Get {@link OCShare} instance from DB and updates the UI. - */ - private void refreshUiFromDB() { - refreshSharesFromDB(); - // Updates UI with new state - setupView(); - } - - private void unShareWith(OCShare share) { - fileOperationsHelper.unShareShare(file, share.getId()); - } - - /** - * Starts a dialog that requests a password to the user to protect a share link. - * - * @param createShare When 'true', the request for password will be followed by the creation of a new public - * link; when 'false', a public share is assumed to exist, and the password is bound to it. - * @param askForPassword if true, password is optional - */ - public void requestPasswordForShareViaLink(boolean createShare, boolean askForPassword) { - SharePasswordDialogFragment dialog = SharePasswordDialogFragment.newInstance(file, - createShare, - askForPassword); - dialog.show(getChildFragmentManager(), SharePasswordDialogFragment.PASSWORD_FRAGMENT); - } - - @Override - public void requestPasswordForShare(OCShare share, boolean askForPassword) { - SharePasswordDialogFragment dialog = SharePasswordDialogFragment.newInstance(share, askForPassword); - dialog.show(getChildFragmentManager(), SharePasswordDialogFragment.PASSWORD_FRAGMENT); - } - - @Override - public void showProfileBottomSheet(User user, String shareWith) { - if (user.getServer().getVersion().isNewerOrEqual(NextcloudVersion.nextcloud_23)) { - new RetrieveHoverCardAsyncTask(user, - shareWith, - fileActivity, - clientFactory, - viewThemeUtils).execute(); - } - } - - public void refreshCapabilitiesFromDB() { - capabilities = fileDataStorageManager.getCapability(user.getAccountName()); - } - - /** - * Get public link from the DB to fill in the "Share link" section in the UI. Takes into account server capabilities - * before reading database. - */ - @SuppressFBWarnings("PSC") - public void refreshSharesFromDB() { - if (binding == null) { - return; - } - - OCFile newFile = fileDataStorageManager.getFileById(file.getFileId()); - if (newFile != null) { - file = newFile; - } - - if (internalShareeListAdapter == null) { - DisplayUtils.showSnackMessage(this, R.string.could_not_retrieve_shares); - return; - } - - internalShareeListAdapter.removeAll(); - - // to show share with users/groups info - List shares = fileDataStorageManager.getSharesWithForAFile(file.getRemotePath(), - user.getAccountName()); - - List internalShares = new ArrayList<>(); - List externalShares = new ArrayList<>(); - - for (OCShare share : shares) { - if (share.getShareType() != null) { - switch (share.getShareType()) { - case PUBLIC_LINK: - case FEDERATED_GROUP: - case FEDERATED: - case EMAIL: - externalShares.add(share); - break; - - default: - internalShares.add(share); - break; - } - } - } - - internalShareeListAdapter.addShares(internalShares); - ViewExtensionsKt.setVisibleIf(binding.sharesListInternalShowAll, internalShareeListAdapter.shares.size() > 3); - - addExternalAndPublicShares(externalShares); - ViewExtensionsKt.setVisibleIf(binding.sharesListExternalShowAll, externalShareeListAdapter.shares.size() > 3); - } - - private void addExternalAndPublicShares(List externalShares) { - final var publicShares = fileDataStorageManager.getSharesByPathAndType(file.getRemotePath(), ShareType.PUBLIC_LINK, ""); - externalShareeListAdapter.removeAll(); - final var shares = OCShareExtensionsKt.mergeDistinctByToken(externalShares, publicShares); - externalShareeListAdapter.addShares(shares); - } - - private void checkContactPermission() { - if (PermissionUtil.checkSelfPermission(requireActivity(), Manifest.permission.READ_CONTACTS)) { - pickContactEmail(); - } else { - requestContactPermissionLauncher.launch(Manifest.permission.READ_CONTACTS); - } - } - - private void pickContactEmail() { - Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Email.CONTENT_URI); - - if (intent.resolveActivity(requireContext().getPackageManager()) != null) { - onContactSelectionResultLauncher.launch(intent); - } else { - DisplayUtils.showSnackMessage(this, R.string.file_detail_sharing_fragment_no_contact_app_message); - } - } - - private void handleContactResult(@NonNull Uri contactUri) { - // Define the projection to get all email addresses. - String[] projection = {ContactsContract.CommonDataKinds.Email.ADDRESS}; - - Cursor cursor = fileActivity.getContentResolver().query(contactUri, projection, null, null, null); - - if (cursor != null) { - if (cursor.moveToFirst()) { - // The contact has only one email address, use it. - int columnIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.ADDRESS); - if (columnIndex != -1) { - // Use the email address as needed. - // email variable contains the selected contact's email address. - String email = cursor.getString(columnIndex); - binding.searchView.post(() -> { - if (binding == null) { - return; - } - - binding.searchView.setQuery(email, false); - binding.searchView.requestFocus(); - }); - } else { - DisplayUtils.showSnackMessage(this, R.string.email_pick_failed); - Log_OC.e(FileDetailSharingFragment.class.getSimpleName(), "Failed to pick email address."); - } - } else { - DisplayUtils.showSnackMessage(this, R.string.email_pick_failed); - Log_OC.e(FileDetailSharingFragment.class.getSimpleName(), "Failed to pick email address as no Email found."); - } - cursor.close(); - } else { - DisplayUtils.showSnackMessage(this, R.string.email_pick_failed); - Log_OC.e(FileDetailSharingFragment.class.getSimpleName(), "Failed to pick email address as Cursor is null."); - } - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - outState.putParcelable(ARG_FILE, file); - outState.putParcelable(ARG_USER, user); - } - - @Override - public void avatarGenerated(Drawable avatarDrawable, Object callContext) { - if (binding == null) { - return; - } - binding.sharedWithYouAvatar.setImageDrawable(avatarDrawable); - } - - @Override - public boolean shouldCallGeneratedCallback(String tag, Object callContext) { - return false; - } - - private boolean isReshareForbidden(OCShare share) { - return ShareType.FEDERATED == share.getShareType() || - capabilities != null && capabilities.getFilesSharingResharing().isFalse(); - } - - @VisibleForTesting - public void search(String query) { - SearchView searchView = requireView().findViewById(R.id.searchView); - searchView.setQuery(query, true); - } - - @Override - public void advancedPermissions(OCShare share) { - modifyExistingShare(share, FileDetailsSharingProcessFragment.SCREEN_TYPE_PERMISSION); - } - - @Override - public void sendNewEmail(OCShare share) { - modifyExistingShare(share, FileDetailsSharingProcessFragment.SCREEN_TYPE_NOTE); - } - - @Override - public void unShare(OCShare share) { - if (binding == null) { - return; - } - - unShareWith(share); - - FileEntity entity = fileDataStorageManager.getFileEntity(file); - - if (binding.sharesListInternal.getAdapter() instanceof ShareeListAdapter adapter) { - adapter.remove(share); - if (entity != null && adapter.isAdapterEmpty()) { - entity.setSharedWithSharee(0); - fileDataStorageManager.updateFileEntity(entity); - } - } else if (binding.sharesListExternal.getAdapter() instanceof ShareeListAdapter adapter) { - adapter.remove(share); - if (entity != null && adapter.isAdapterEmpty()) { - entity.setSharedViaLink(0); - fileDataStorageManager.updateFileEntity(entity); - } - } else { - DisplayUtils.showSnackMessage(this, R.string.failed_update_ui); - } - } - - @Override - public void sendLink(OCShare share) { - if (file.isSharedViaLink() && !TextUtils.isEmpty(share.getShareLink())) { - FileDisplayActivity.showShareLinkDialog(fileActivity, file, share.getShareLink()); - } else { - showSendLinkTo(share); - } - } - - @Override - public void addAnotherLink(OCShare share) { - createPublicShareLink(); - } - - private void modifyExistingShare(OCShare share, int screenTypePermission) { - onEditShareListener.editExistingShare(share, screenTypePermission, !isReshareForbidden(share)); - } - - @Override - public void onQuickPermissionChanged(OCShare share, int permission) { - fileOperationsHelper.setPermissionsToShare(share, permission); - } - - @Override - public void openShareDetailWithCustomPermissions(OCShare share) { - modifyExistingShare(share, FileDetailsSharingProcessFragment.SCREEN_TYPE_PERMISSION_WITH_CUSTOM_PERMISSION); - } - - //launcher for contact permission - private final ActivityResultLauncher requestContactPermissionLauncher = - registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> { - if (isGranted) { - pickContactEmail(); - } else { - DisplayUtils.showSnackMessage(this, R.string.contact_no_permission); - } - }); - - //launcher to handle contact selection - private final ActivityResultLauncher onContactSelectionResultLauncher = - registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), - result -> { - if (result.getResultCode() == Activity.RESULT_OK) { - Intent intent = result.getData(); - if (intent == null) { - DisplayUtils.showSnackMessage(this, R.string.email_pick_failed); - return; - } - - Uri contactUri = intent.getData(); - if (contactUri == null) { - DisplayUtils.showSnackMessage(this, R.string.email_pick_failed); - return; - } - - handleContactResult(contactUri); - - } - }); - - public interface OnEditShareListener { - void editExistingShare(OCShare share, int screenTypePermission, boolean isReshareShown); - - void onShareProcessClosed(); - } -} diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt index 9630c28b7250..32ecc35b3c67 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt @@ -424,8 +424,7 @@ class FileDetailSharingFragment : val client = clientFactory.create(user) val metadata = RefreshFolderOperation.getDecryptedFolderMetadata(true, file, client, user, context) if (metadata is DecryptedFolderMetadataFile) { - file?.setE2eCounter(metadata.metadata.counter) - fileDataStorageManager?.saveFile(file) + fileDataStorageManager?.updateE2EECounter(file, metadata) } true } catch (e: Exception) { From 217f1b1536db6da57d4747e465293f3f107caea5 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 9 Jul 2026 10:33:50 +0200 Subject: [PATCH 6/6] wip Signed-off-by: alperozturk96 --- .../operations/RefreshFolderOperation.java | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java b/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java index 660045f443cc..76ec59a45230 100644 --- a/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java @@ -538,19 +538,9 @@ private void synchronizeData(List folderAndFiles) { final var workManager = WorkManager.getInstance(mContext); final var tag = BackgroundJobManagerImpl.JOB_FILES_UPLOAD + user.getAccountName(); boolean isWorkScheduled = WorkManagerExtensionsKt.isWorkScheduled(workManager, tag); - - Log_OC.d(TAG, "file upload worker status: " + isWorkScheduled); - - // wait until current uploads finish then verify metadata - while (isWorkScheduled) { - try { - Thread.sleep(500); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - break; - } - isWorkScheduled = WorkManagerExtensionsKt.isWorkScheduled(workManager, tag); - Log_OC.d(TAG, "file upload worker updated status: " + isWorkScheduled); + if (isWorkScheduled) { + Log_OC.d(TAG, "Upload worker running for " + user.getAccountName() + "; deferring metadata/counter update to next refresh"); + return; } object = getDecryptedFolderMetadata(encryptedAncestor,