From afb55de9640aadbc61309a3980608356676516bc Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 26 Oct 2026 19:13:29 +0300 Subject: [PATCH 1/4] Rename .java to .kt Signed-off-by: alperozturk96 --- .../fragment/{OCFileListFragment.java => OCFileListFragment.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/com/owncloud/android/ui/fragment/{OCFileListFragment.java => OCFileListFragment.kt} (100%) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt similarity index 100% rename from app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java rename to app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt From e25410438f6386729173b16f7b4a39d2f6a8f0f5 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 26 Oct 2026 19:13:30 +0300 Subject: [PATCH 2/4] refactor(oc-file-list-fragment): kotlin Signed-off-by: alperozturk96 --- .../OCFileListFragmentStaticServerIT.kt | 19 +- .../ui/fragment/SharedListFragmentIT.kt | 2 +- .../OCFileListFragmentExtensions.kt | 2 +- .../ui/activity/FileDisplayActivity.kt | 8 +- .../fragment/EncryptedFolderClickHandler.kt | 9 +- .../ui/fragment/FileListLayoutManager.kt | 12 +- .../android/ui/fragment/FolderEncryption.kt | 14 +- .../android/ui/fragment/GalleryFragment.kt | 46 +- .../ui/fragment/GroupfolderListFragment.kt | 15 +- .../android/ui/fragment/OCFileListFragment.kt | 2960 +++++++---------- .../ui/fragment/OCFileListSearchTask.kt | 8 +- .../android/ui/fragment/SharedListFragment.kt | 28 +- .../ui/fragment/albums/AlbumItemsFragment.kt | 2 - .../OCFileListFragmentInterface.java | 2 - 14 files changed, 1362 insertions(+), 1765 deletions(-) diff --git a/app/src/androidTest/java/com/owncloud/android/ui/fragment/OCFileListFragmentStaticServerIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/fragment/OCFileListFragmentStaticServerIT.kt index fa9a350f41a6..5cfcf18e1b7f 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/fragment/OCFileListFragmentStaticServerIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/fragment/OCFileListFragmentStaticServerIT.kt @@ -246,7 +246,7 @@ class OCFileListFragmentStaticServerIT : AbstractIT() { sut.supportFragmentManager.executePendingTransactions() val root = sut.storageManager.getFileByEncryptedRemotePath("/") fragment.listDirectory(root, false) - fragment.adapter.setShowShareAvatar(true) + requireNotNull(fragment.adapter).setShowShareAvatar(true) } val screenShotName = createName(testClassName + "_" + "showSharedFiles", "") @@ -334,7 +334,7 @@ class OCFileListFragmentStaticServerIT : AbstractIT() { sut.supportFragmentManager.executePendingTransactions() val root = sut.storageManager.getFileByEncryptedRemotePath("/") fragment.listDirectory(root, false) - fragment.adapter.setShowShareAvatar(true) + requireNotNull(fragment.adapter).setShowShareAvatar(true) } val screenShotName = createName(testClassName + "_" + "showFolderTypes", "") @@ -399,27 +399,28 @@ class OCFileListFragmentStaticServerIT : AbstractIT() { } val testFolder: OCFile = activity!!.storageManager.getFileByEncryptedRemotePath("/test/") + val adapter = requireNotNull(sut.adapter) // richWorkspace is not set - Assert.assertFalse(sut.adapter.shouldShowHeader()) + Assert.assertFalse(adapter.shouldShowHeader()) testFolder.richWorkspace = " " activity.storageManager.saveFile(testFolder) - sut.adapter.swapDirectory(user, testFolder, activity.storageManager, false, "") + adapter.swapDirectory(user, testFolder, activity.storageManager, false, "") - Assert.assertFalse(sut.adapter.shouldShowHeader()) + Assert.assertFalse(adapter.shouldShowHeader()) testFolder.richWorkspace = null activity.storageManager.saveFile(testFolder) - sut.adapter.swapDirectory(user, testFolder, activity.storageManager, false, "") - Assert.assertFalse(sut.adapter.shouldShowHeader()) + adapter.swapDirectory(user, testFolder, activity.storageManager, false, "") + Assert.assertFalse(adapter.shouldShowHeader()) EspressoIdlingResource.increment() testFolder.richWorkspace = "1" activity.storageManager.saveFile(testFolder) - sut.adapter.setCurrentDirectory(testFolder) + adapter.setCurrentDirectory(testFolder) - Assert.assertTrue(sut.adapter.shouldShowHeader()) + Assert.assertTrue(adapter.shouldShowHeader()) } } diff --git a/app/src/androidTest/java/com/owncloud/android/ui/fragment/SharedListFragmentIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/fragment/SharedListFragmentIT.kt index 9bff0348fca8..a2cd480eb26e 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/fragment/SharedListFragmentIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/fragment/SharedListFragmentIT.kt @@ -170,7 +170,7 @@ internal class SharedListFragmentIT : AbstractIT() { OCShareToOCFileConverter .parseAndSaveShares(listOf(), shares, storageManager, user) } - fragment.adapter.run { + requireNotNull(fragment.adapter).run { prepareForSearchData(SearchType.SHARED_FILTER) updateAdapter(newList, null) } diff --git a/app/src/main/java/com/nextcloud/utils/extensions/OCFileListFragmentExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/OCFileListFragmentExtensions.kt index 08079b46fe54..f333c92c7f60 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/OCFileListFragmentExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/OCFileListFragmentExtensions.kt @@ -84,7 +84,7 @@ private fun OCFileListFragment.folderFromResult(bundle: Bundle): OCFile? { return null } - val file = containerActivity.storageManager.getFileByEncryptedRemotePath(remotePath) + val file = requireContainerActivity().storageManager.getFileByEncryptedRemotePath(remotePath) if (file == null) { Log_OC.e(TAG, "file is null, cannot continue encryption action") } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt index 7c962148087d..a207717dbd49 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt @@ -674,7 +674,7 @@ class FileDisplayActivity : ON_DEVICE == action -> { refreshOrInitOCFileListFragment() - listOfFilesFragment?.setCurrentSearchType(SearchType.ON_DEVICE) + listOfFilesFragment?.currentSearchType = SearchType.ON_DEVICE updateActionBarTitleAndHomeButton(null) } } @@ -833,7 +833,7 @@ class FileDisplayActivity : getOCFileListFragmentFromFile(object : TransactionInterface { override fun onOCFileListFragmentComplete(fragment: OCFileListFragment) { browseUp(fragment) - fragment.onOverflowIconClicked(file, null) + file?.let { fragment.onOverflowIconClicked(it, null) } } }) } @@ -2533,7 +2533,9 @@ class FileDisplayActivity : return } - fileListFragment?.onItemClicked(storageManager.getFileByDecryptedRemotePath(operation.getRemotePath())) + storageManager.getFileByDecryptedRemotePath(operation.getRemotePath())?.let { + fileListFragment?.onItemClicked(it) + } } else { try { if (RemoteOperationResult.ResultCode.FOLDER_ALREADY_EXISTS == result.code) { diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/EncryptedFolderClickHandler.kt b/app/src/main/java/com/owncloud/android/ui/fragment/EncryptedFolderClickHandler.kt index 905be036e284..64db2b2a59df 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/EncryptedFolderClickHandler.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/EncryptedFolderClickHandler.kt @@ -117,7 +117,8 @@ class EncryptedFolderClickHandler(private val fragment: OCFileListFragment) { fun openAfterKeySetup(file: OCFile) { fragment.lifecycleScope.launch { if (fragment.e2eeActionResolver.checkFolderMetadataKey(file)) { - onEncryptionSetupComplete(file, fragment.adapter.getItemPosition(file)) + val position = fragment.adapter?.getItemPosition(file) ?: return@launch + onEncryptionSetupComplete(file, position) } else { SnackbarUtil.show(fragment, R.string.encryption_open_key_mismatch) } @@ -131,7 +132,7 @@ class EncryptedFolderClickHandler(private val fragment: OCFileListFragment) { private fun onFolderKeyVerified(file: OCFile, position: Int, fileActivity: FileActivity) { val user = fileActivity.user.orElseThrow { RuntimeException() } - val capability = fragment.containerActivity.getStorageManager().getCapability(user.accountName) + val capability = fragment.requireContainerActivity().storageManager.getCapability(user.accountName) if (capability.endToEndEncryption.isFalse || capability.endToEndEncryption.isUnknown) { SnackbarUtil.show(fragment, R.string.end_to_end_encryption_not_enabled) @@ -146,8 +147,8 @@ class EncryptedFolderClickHandler(private val fragment: OCFileListFragment) { } private fun onEncryptionSetupComplete(file: OCFile?, position: Int) { - fragment.searchFragment = false - fragment.mHideFab = false + fragment.isSearchFragment = false + fragment.hideFab = false val folderPickerActivity = fragment.containerActivity as? FolderPickerActivity if (folderPickerActivity?.isDoNotEnterEncryptedFolder == true) { diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileListLayoutManager.kt b/app/src/main/java/com/owncloud/android/ui/fragment/FileListLayoutManager.kt index 027acdd4c272..8b8808c39ebf 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileListLayoutManager.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileListLayoutManager.kt @@ -24,11 +24,11 @@ class FileListLayoutManager(private val fragment: OCFileListFragment, private va fun sortFiles(sortOrder: FileSortOrder?) { sortOrder ?: return fragment.mSortButton?.setText(SortOrderUi.labelRes(sortOrder)) - fragment.mAdapter.setSortOrder(fragment.mFile, sortOrder) + fragment.adapter?.setSortOrder(fragment.currentFile, sortOrder) } fun isGridViewPreferred(): Boolean { - val layout = FolderLayout.get(fragment.mFile, fragment.currentSearchType) + val layout = FolderLayout.get(fragment.currentFile, fragment.currentSearchType) return preferences.getFolderLayout(layout) == OCFileListFragment.FOLDER_LAYOUT_GRID } @@ -45,7 +45,7 @@ class FileListLayoutManager(private val fragment: OCFileListFragment, private va } fun setListAsPreferred() { - val layout = FolderLayout.get(fragment.mFile, fragment.currentSearchType) + val layout = FolderLayout.get(fragment.currentFile, fragment.currentSearchType) preferences.setFolderLayout(layout, OCFileListFragment.FOLDER_LAYOUT_LIST) switchToListView() } @@ -57,7 +57,7 @@ class FileListLayoutManager(private val fragment: OCFileListFragment, private va } fun setGridAsPreferred() { - val layout = FolderLayout.get(fragment.mFile, fragment.currentSearchType) + val layout = FolderLayout.get(fragment.currentFile, fragment.currentSearchType) preferences.setFolderLayout(layout, OCFileListFragment.FOLDER_LAYOUT_GRID) switchToGridView() } @@ -90,8 +90,8 @@ class FileListLayoutManager(private val fragment: OCFileListFragment, private va if (grid) { layoutManager = GridLayoutManager(context, fragment.columnsCount) layoutManager.spanSizeLookup = object : SpanSizeLookup() { - override fun getSpanSize(position: Int): Int = if (position == fragment.adapter.itemCount - 1 || - (position == 0 && fragment.adapter.isHeaderDisplayed) + override fun getSpanSize(position: Int): Int = if (position == adapter.itemCount - 1 || + (position == 0 && adapter.isHeaderDisplayed) ) { layoutManager.spanCount } else { diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FolderEncryption.kt b/app/src/main/java/com/owncloud/android/ui/fragment/FolderEncryption.kt index 30fc3d797da0..ac52f7cab1b8 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FolderEncryption.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FolderEncryption.kt @@ -35,7 +35,7 @@ class FolderEncryption(private val fragment: OCFileListFragment) { val shouldBeEncrypted = event.shouldBeEncrypted try { - val storageManager = fragment.containerActivity.storageManager + val storageManager = fragment.requireContainerActivity().storageManager val folder = storageManager.getFileByRemoteId(remoteId) ?: run { Log_OC.e(TAG, "folder is null, cannot encrypt") return@withContext false @@ -101,13 +101,13 @@ class FolderEncryption(private val fragment: OCFileListFragment) { ) withContext(Dispatchers.Main) { - val isFileExists = (fragment.adapter.getFileByRemoteId(remoteId) != null) - if (!isFileExists) { - val newFile = storageManager.getFileByRemoteId(remoteId) - fragment.adapter.insertFile(newFile) - } + fragment.adapter?.run { + if (getFileByRemoteId(remoteId) == null) { + insertFile(storageManager.getFileByRemoteId(remoteId)) + } - fragment.adapter.updateFileEncryptionById(remoteId, shouldBeEncrypted) + updateFileEncryptionById(remoteId, shouldBeEncrypted) + } } return result diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/GalleryFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/GalleryFragment.kt index 9ac86cb6555a..f515dabda3e6 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/GalleryFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/GalleryFragment.kt @@ -75,7 +75,7 @@ class GalleryFragment : private var loadedItemCount = INITIAL_GALLERY_WINDOW private var restoreScrollPending = false private var scrollAnchorFile: OCFile? = null - private var adapter: GalleryAdapter? = null + private var galleryAdapter: GalleryAdapter? = null private var bottomSheet: GalleryFragmentBottomSheetDialog? = null @@ -98,7 +98,7 @@ class GalleryFragment : override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - searchFragment = true + isSearchFragment = true arguments?.let { isFromAlbum = it.getBoolean(AlbumsPickerActivity.EXTRA_FROM_ALBUM, false) } @@ -164,7 +164,7 @@ class GalleryFragment : binding?.paginationLoader?.removeCallbacks(showPaginationLoader) - adapter = null + galleryAdapter = null super.onDestroyView() } @@ -214,18 +214,18 @@ class GalleryFragment : } override fun setAdapter(args: Bundle?) { - adapter = GalleryAdapter( + galleryAdapter = GalleryAdapter( requireContext(), accountManager.user, this, - containerActivity, + requireContainerActivity(), viewThemeUtils, this.columnsCount, ThumbnailsCacheManager.getThumbnailDimension(), thumbnailGenerator ) - adapter?.setHasStableIds(true) - setRecyclerViewAdapter(adapter) + galleryAdapter?.setHasStableIds(true) + setRecyclerViewAdapter(galleryAdapter) // update the footer as there is no footer shown in media view if (recyclerView is EmptyRecyclerView) { @@ -237,7 +237,7 @@ class GalleryFragment : } val layoutManager = GridLayoutManager(context, 1) - adapter?.setLayoutManager(layoutManager) + galleryAdapter?.setLayoutManager(layoutManager) recyclerView?.run { setLayoutManager(layoutManager) setItemViewCacheSize(ITEM_VIEW_CACHE_SIZE) @@ -248,7 +248,7 @@ class GalleryFragment : override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) columnsCount = defaultColumnsCount(newConfig.isLandscape()) - adapter?.changeColumn(columnsCount) + galleryAdapter?.changeColumn(columnsCount) showAllGalleryItems() } @@ -258,7 +258,8 @@ class GalleryFragment : handleSearchEvent() } - override fun getCommonAdapter(): CommonOCFileListAdapterInterface? = adapter + override val commonAdapter: CommonOCFileListAdapterInterface? + get() = galleryAdapter val currentMediaState: MediaState? get() = bottomSheet?.currMediaState @@ -278,7 +279,7 @@ class GalleryFragment : private fun handleSearchEvent() { prepareCurrentSearch(searchEvent) - if (adapter?.isEmpty() != false) { + if (galleryAdapter?.isEmpty() != false) { setEmptyListMessage(EmptyListState.LOADING) } @@ -305,7 +306,7 @@ class GalleryFragment : val loader = binding?.paginationLoader ?: return loader.removeCallbacks(showPaginationLoader) - if (isLoadingNextPage && adapter?.isEmpty() == false) { + if (isLoadingNextPage && galleryAdapter?.isEmpty() == false) { loader.postDelayed(showPaginationLoader, PAGINATION_LOADER_DELAY_IN_MS) } else { loader.setVisibleIf(false) @@ -326,7 +327,7 @@ class GalleryFragment : endDate = result.lastTimestamp } - if (adapter?.isEmpty() == true) { + if (galleryAdapter?.isEmpty() == true) { setEmptyListMessage(SearchType.GALLERY_SEARCH) } @@ -369,6 +370,7 @@ class GalleryFragment : } private fun runGallerySearchTask() { + val containerActivity = containerActivity if (containerActivity == null) { Log_OC.w(TAG, "container activity is null, can't run search task") return @@ -379,7 +381,7 @@ class GalleryFragment : photoSearchTask = GallerySearchTask( this, accountManager.user, - containerActivity.getStorageManager(), + containerActivity.storageManager, endDate, limit ).execute() @@ -405,7 +407,7 @@ class GalleryFragment : return } - val lastFile = adapter?.getItem(lastVisibleItem - 1) ?: return + val lastFile = galleryAdapter?.getItem(lastVisibleItem - 1) ?: return val lastItemTimestamp = lastFile.modificationTimestamp / MILLIS_PER_SECOND // if we have already older media in the gallery then retrieve file in chronological order to fill the gap @@ -441,7 +443,7 @@ class GalleryFragment : fun showAllGalleryItems() { val mediaState = bottomSheet?.currMediaState ?: return - val rowLayout = adapter?.rowLayout() ?: return + val rowLayout = galleryAdapter?.rowLayout() ?: return val mimeFilter = when (mediaState) { MediaState.MEDIA_STATE_PHOTOS_ONLY -> IMAGE_MIME_FILTER @@ -452,7 +454,7 @@ class GalleryFragment : showGalleryJob?.cancel() showGalleryJob = lifecycleScope.launch(Dispatchers.Default) { val remotePath = preferences.getLastSelectedMediaFolder() - val items = containerActivity.storageManager.getGalleryItemsPageSuspended( + val items = requireContainerActivity().storageManager.getGalleryItemsPageSuspended( remotePath, mimeFilter, loadedItemCount @@ -464,7 +466,7 @@ class GalleryFragment : if (galleryItems.isEmpty()) { setEmptyListMessage(SearchType.GALLERY_SEARCH) } - adapter?.updateList(galleryItems) + galleryAdapter?.updateList(galleryItems) updateSubtitle(mediaState) scrollToAnchorFile() @@ -550,7 +552,7 @@ class GalleryFragment : } columnsCount = updated - adapter?.changeColumn(columnsCount) + galleryAdapter?.changeColumn(columnsCount) scrollAnchorFile = scrollAnchorFile ?: firstVisibleFile() showAllGalleryItems() } @@ -563,14 +565,14 @@ class GalleryFragment : return null } - return (first..last).firstNotNullOfOrNull { adapter?.getItem(it) } + return (first..last).firstNotNullOfOrNull { galleryAdapter?.getItem(it) } } private fun scrollToAnchorFile() { val file = scrollAnchorFile ?: return scrollAnchorFile = null - val position = adapter?.getItemPosition(file) ?: return + val position = galleryAdapter?.getItemPosition(file) ?: return if (position < 0) { return } @@ -579,7 +581,7 @@ class GalleryFragment : } fun markAsFavorite(remotePath: String, favorite: Boolean) { - adapter?.markAsFavorite(remotePath, favorite) + galleryAdapter?.markAsFavorite(remotePath, favorite) } companion object { diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/GroupfolderListFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/GroupfolderListFragment.kt index 68bb888306ec..4b186ef95806 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/GroupfolderListFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/GroupfolderListFragment.kt @@ -33,14 +33,14 @@ class GroupfolderListFragment : Injectable, GroupfolderListInterface { - lateinit var adapter: GroupFolderListAdapter + private lateinit var groupFolderAdapter: GroupFolderListAdapter @Inject lateinit var logger: Logger override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - searchFragment = true + isSearchFragment = true } @Deprecated("Deprecated in Java") @@ -55,8 +55,9 @@ class GroupfolderListFragment : } public override fun setAdapter(args: Bundle?) { - adapter = GroupFolderListAdapter(requireContext(), viewThemeUtils, this, preferences.isDarkModeEnabled) - setRecyclerViewAdapter(adapter) + groupFolderAdapter = + GroupFolderListAdapter(requireContext(), viewThemeUtils, this, preferences.isDarkModeEnabled) + setRecyclerViewAdapter(groupFolderAdapter) val layoutManager = LinearLayoutManager(context) recyclerView?.layoutManager = layoutManager @@ -67,7 +68,7 @@ class GroupfolderListFragment : GroupfoldersSearchTask( this, accountManager.user, - containerActivity.storageManager + requireContainerActivity().storageManager ).execute() } @@ -92,8 +93,8 @@ class GroupfolderListFragment : @SuppressLint("NotifyDataSetChanged") fun setData(result: Map) { - adapter.setData(result) - adapter.notifyDataSetChanged() + groupFolderAdapter.setData(result) + groupFolderAdapter.notifyDataSetChanged() } override fun onFolderClick(path: String) { diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt index 6e42ac67cd7c..5c0e385c9f31 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt @@ -14,2137 +14,1733 @@ * SPDX-FileCopyrightText: 2011-2012 Bartosz Przybylski * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ -package com.owncloud.android.ui.fragment; - -import android.Manifest; -import android.annotation.SuppressLint; -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.text.TextUtils; -import android.util.Log; -import android.view.ActionMode; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AbsListView; - -import com.google.android.material.bottomsheet.BottomSheetBehavior; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.android.material.floatingactionbutton.FloatingActionButton; -import com.google.android.material.snackbar.Snackbar; -import com.nextcloud.android.lib.resources.clientintegration.Endpoint; -import com.nextcloud.android.lib.resources.files.ToggleFileLockRemoteOperation; -import com.nextcloud.client.account.User; -import com.nextcloud.client.device.DeviceInfo; -import com.nextcloud.client.di.Injectable; -import com.nextcloud.client.documentscan.AppScanOptionalFeature; -import com.nextcloud.client.documentscan.DocumentScanActivity; -import com.nextcloud.client.jobs.BackgroundJobManager; -import com.nextcloud.client.network.ClientFactory; -import com.nextcloud.client.systembars.SystemBarBackgroundCallbacks; -import com.nextcloud.client.utils.Throttler; -import com.nextcloud.common.NextcloudClient; -import com.nextcloud.ui.fileactions.FileAction; -import com.nextcloud.ui.fileactions.FileActionHandler; -import com.nextcloud.ui.fileactions.FileActionHost; -import com.nextcloud.ui.fileactions.FileActionsBottomSheet; -import com.nextcloud.ui.sort.SortOrderUi; -import com.nextcloud.utils.AppConstants; -import com.nextcloud.utils.EditorUtils; -import com.nextcloud.utils.SnackbarUtil; -import com.nextcloud.utils.avatar.AvatarGenerator; -import com.nextcloud.utils.e2ee.E2EEActionResolver; -import com.nextcloud.utils.e2ee.E2EEDialogPresenter; -import com.nextcloud.utils.extensions.BundleExtensionsKt; -import com.nextcloud.utils.extensions.ContextExtensionsKt; -import com.nextcloud.utils.extensions.FileExtensionsKt; -import com.nextcloud.utils.extensions.FragmentExtensionsKt; -import com.nextcloud.utils.extensions.IntentExtensionsKt; -import com.nextcloud.utils.extensions.MimeTypeExtensionsKt; -import com.nextcloud.utils.extensions.OCFileExtensionsKt; -import com.nextcloud.utils.extensions.OCFileListFragmentExtensionsKt; -import com.nextcloud.utils.extensions.ViewExtensionsKt; -import com.nextcloud.utils.fileNameValidator.FileNameValidator; -import com.nextcloud.utils.thumbnail.ThumbnailGenerator; -import com.nextcloud.utils.view.FastScrollUtils; -import com.owncloud.android.MainApp; -import com.owncloud.android.R; -import com.owncloud.android.datamodel.ArbitraryDataProvider; -import com.owncloud.android.datamodel.FileDataStorageManager; -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.datamodel.OCFileDepth; -import com.owncloud.android.datamodel.SyncedFolderProvider; -import com.owncloud.android.lib.common.Creator; -import com.owncloud.android.lib.common.OwnCloudClient; -import com.owncloud.android.lib.common.operations.RemoteOperation; -import com.owncloud.android.lib.common.operations.RemoteOperationResult; -import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.lib.resources.files.SearchRemoteOperation; -import com.owncloud.android.lib.resources.files.ToggleFavoriteRemoteOperation; -import com.owncloud.android.lib.resources.status.OCCapability; -import com.owncloud.android.lib.resources.status.Type; -import com.owncloud.android.ui.activity.AlbumsPickerActivity; -import com.owncloud.android.ui.activity.DrawerActivity; -import com.owncloud.android.ui.activity.FileActivity; -import com.owncloud.android.ui.activity.FileDisplayActivity; -import com.owncloud.android.ui.activity.FolderPickerActivity; -import com.owncloud.android.ui.activity.OnEnforceableRefreshListener; -import com.owncloud.android.ui.activity.TextEditorWebView; -import com.owncloud.android.ui.activity.UploadFilesActivity; -import com.owncloud.android.ui.adapter.filelist.CommonOCFileListAdapterInterface; -import com.owncloud.android.ui.adapter.filelist.OCFileListAdapter; -import com.owncloud.android.ui.dialog.ChooseRichDocumentsTemplateDialogFragment; -import com.owncloud.android.ui.dialog.ChooseTemplateDialogFragment; -import com.owncloud.android.ui.dialog.ConfirmationDialogFragment; -import com.owncloud.android.ui.dialog.CreateFolderDialogFragment; -import com.owncloud.android.ui.dialog.SyncFileNotEnoughSpaceDialogFragment; -import com.owncloud.android.ui.events.ChangeMenuEvent; -import com.owncloud.android.ui.events.CommentsEvent; -import com.owncloud.android.ui.events.EncryptionEvent; -import com.owncloud.android.ui.events.FavoriteEvent; -import com.owncloud.android.ui.events.FileLockEvent; -import com.owncloud.android.ui.events.SearchEvent; -import com.owncloud.android.ui.fragment.helper.ParentFolderFinder; -import com.owncloud.android.ui.helpers.FileOperationsHelper; -import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface; -import com.owncloud.android.utils.FileSortOrder; -import com.owncloud.android.utils.FileStorageUtils; -import com.owncloud.android.utils.PermissionUtil; -import com.owncloud.android.utils.theme.ThemeUtils; - -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -import javax.inject.Inject; - -import androidx.annotation.IdRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.OptIn; -import androidx.core.content.ContextCompat; -import androidx.drawerlayout.widget.DrawerLayout; -import androidx.fragment.app.FragmentActivity; -import androidx.media3.common.util.UnstableApi; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import kotlin.Unit; - -import static com.owncloud.android.datamodel.OCFile.ROOT_PATH; -import static com.owncloud.android.ui.fragment.SearchType.FAVORITE_SEARCH; -import static com.owncloud.android.ui.fragment.SearchType.FILE_SEARCH; -import static com.owncloud.android.ui.fragment.SearchType.NO_SEARCH; -import static com.owncloud.android.ui.fragment.SearchType.RECENT_FILES_SEARCH; -import static com.owncloud.android.ui.fragment.SearchType.SHARED_FILTER; +package com.owncloud.android.ui.fragment + +import android.Manifest +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.view.ActionMode +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.AbsListView +import androidx.annotation.IdRes +import androidx.annotation.OptIn +import androidx.annotation.StringRes +import androidx.core.content.ContextCompat +import androidx.drawerlayout.widget.DrawerLayout +import androidx.media3.common.util.UnstableApi +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.floatingactionbutton.FloatingActionButton +import com.google.android.material.snackbar.Snackbar +import com.nextcloud.android.lib.resources.files.ToggleFileLockRemoteOperation +import com.nextcloud.client.account.User +import com.nextcloud.client.device.DeviceInfo +import com.nextcloud.client.di.Injectable +import com.nextcloud.client.documentscan.AppScanOptionalFeature +import com.nextcloud.client.documentscan.DocumentScanActivity +import com.nextcloud.client.jobs.BackgroundJobManager +import com.nextcloud.client.network.ClientFactory +import com.nextcloud.client.systembars.SystemBarBackgroundCallbacks +import com.nextcloud.client.utils.Throttler +import com.nextcloud.ui.fileactions.FileAction +import com.nextcloud.ui.fileactions.FileActionHandler +import com.nextcloud.ui.fileactions.FileActionHost +import com.nextcloud.ui.fileactions.FileActionsBottomSheet +import com.nextcloud.ui.sort.SortOrderUi +import com.nextcloud.utils.AppConstants +import com.nextcloud.utils.EditorUtils +import com.nextcloud.utils.SnackbarUtil +import com.nextcloud.utils.avatar.AvatarGenerator +import com.nextcloud.utils.e2ee.E2EEActionResolver +import com.nextcloud.utils.e2ee.E2EEDialogPresenter +import com.nextcloud.utils.extensions.getDepth +import com.nextcloud.utils.extensions.getParcelableArgument +import com.nextcloud.utils.extensions.isDialogFragmentReady +import com.nextcloud.utils.extensions.isWebViewAvailable +import com.nextcloud.utils.extensions.listenEncryptionDialogResult +import com.nextcloud.utils.extensions.logFileSize +import com.nextcloud.utils.extensions.resolveMimeType +import com.nextcloud.utils.extensions.slideHideBottomBehavior +import com.nextcloud.utils.fileNameValidator.FileNameValidator +import com.nextcloud.utils.thumbnail.ThumbnailGenerator +import com.nextcloud.utils.view.FastScrollUtils +import com.owncloud.android.MainApp +import com.owncloud.android.R +import com.owncloud.android.datamodel.ArbitraryDataProvider +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.datamodel.OCFileDepth +import com.owncloud.android.datamodel.SyncedFolderProvider +import com.owncloud.android.lib.common.Creator +import com.owncloud.android.lib.common.operations.RemoteOperation +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.files.SearchRemoteOperation +import com.owncloud.android.lib.resources.files.ToggleFavoriteRemoteOperation +import com.owncloud.android.lib.resources.status.OCCapability +import com.owncloud.android.lib.resources.status.Type +import com.owncloud.android.ui.CompletionCallback +import com.owncloud.android.ui.activity.AlbumsPickerActivity +import com.owncloud.android.ui.activity.DrawerActivity +import com.owncloud.android.ui.activity.FileActivity +import com.owncloud.android.ui.activity.FileDisplayActivity +import com.owncloud.android.ui.activity.FolderPickerActivity +import com.owncloud.android.ui.activity.OnEnforceableRefreshListener +import com.owncloud.android.ui.activity.TextEditorWebView +import com.owncloud.android.ui.activity.UploadFilesActivity +import com.owncloud.android.ui.adapter.filelist.CommonOCFileListAdapterInterface +import com.owncloud.android.ui.adapter.filelist.OCFileListAdapter +import com.owncloud.android.ui.dialog.ChooseRichDocumentsTemplateDialogFragment +import com.owncloud.android.ui.dialog.ChooseTemplateDialogFragment +import com.owncloud.android.ui.dialog.ConfirmationDialogFragment +import com.owncloud.android.ui.dialog.CreateFolderDialogFragment +import com.owncloud.android.ui.dialog.SyncFileNotEnoughSpaceDialogFragment +import com.owncloud.android.ui.events.ChangeMenuEvent +import com.owncloud.android.ui.events.CommentsEvent +import com.owncloud.android.ui.events.EncryptionEvent +import com.owncloud.android.ui.events.FavoriteEvent +import com.owncloud.android.ui.events.FileLockEvent +import com.owncloud.android.ui.events.SearchEvent +import com.owncloud.android.ui.fragment.FileFragment.ContainerActivity +import com.owncloud.android.ui.fragment.helper.ParentFolderFinder +import com.owncloud.android.ui.helpers.FileOperationsHelper +import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface +import com.owncloud.android.utils.FileSortOrder +import com.owncloud.android.utils.FileStorageUtils +import com.owncloud.android.utils.PermissionUtil +import com.owncloud.android.utils.theme.ThemeUtils +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import kotlin.jvm.optionals.getOrNull +import com.owncloud.android.lib.resources.files.SearchRemoteOperation.SearchType as RemoteSearchType /** * A Fragment that lists all files and folders in a given path. * TODO refactor to get rid of direct dependency on FileDisplayActivity */ -public class OCFileListFragment extends ExtendedListFragment implements +@Suppress("TooManyFunctions", "LargeClass") +open class OCFileListFragment : + ExtendedListFragment(), OCFileListFragmentInterface, OCFileListBottomSheetActions, Injectable { - protected static final String TAG = OCFileListFragment.class.getSimpleName(); - - private static final String MY_PACKAGE = OCFileListFragment.class.getPackage() != null ? - OCFileListFragment.class.getPackage().getName() : "com.owncloud.android.ui.fragment"; - - public final static String ARG_ONLY_FOLDERS_CLICKABLE = MY_PACKAGE + ".ONLY_FOLDERS_CLICKABLE"; - public final static String ARG_FILE_SELECTABLE = MY_PACKAGE + ".FILE_SELECTABLE"; - public final static String ARG_ALLOW_CONTEXTUAL_ACTIONS = MY_PACKAGE + ".ALLOW_CONTEXTUAL"; - public final static String ARG_HIDE_FAB = MY_PACKAGE + ".HIDE_FAB"; - public final static String ARG_HIDE_ITEM_OPTIONS = MY_PACKAGE + ".HIDE_ITEM_OPTIONS"; - public final static String ARG_SEARCH_ONLY_FOLDER = MY_PACKAGE + ".SEARCH_ONLY_FOLDER"; - public final static String ARG_MIMETYPE = MY_PACKAGE + ".MIMETYPE"; - - public static final String DOWNLOAD_SEND = "DOWNLOAD_SEND"; - - - public static final String FOLDER_LAYOUT_LIST = "LIST"; - public static final String FOLDER_LAYOUT_GRID = "GRID"; - - public static final String SEARCH_EVENT = "SEARCH_EVENT"; - private static final String KEY_FILE = MY_PACKAGE + ".extra.FILE"; - public static final String KEY_CURRENT_SEARCH_TYPE = "CURRENT_SEARCH_TYPE"; - - private static final String DIALOG_CREATE_FOLDER = "DIALOG_CREATE_FOLDER"; - private static final String DIALOG_CREATE_DOCUMENT = "DIALOG_CREATE_DOCUMENT"; - private static final String DIALOG_BOTTOM_SHEET = "DIALOG_BOTTOM_SHEET"; - - private static final int SINGLE_SELECTION = 1; - private static final Set SELECTION_ENDING_ACTIONS = Set.of(R.id.action_export_file, - R.id.action_favorite, - R.id.action_unset_favorite, - R.id.action_add_to_album); - private static final int NOT_ENOUGH_SPACE_FRAG_REQUEST_CODE = 2; - - @Inject ClientFactory clientFactory; - @Inject Throttler throttler; - @Inject ThemeUtils themeUtils; - @Inject ArbitraryDataProvider arbitraryDataProvider; - @Inject BackgroundJobManager backgroundJobManager; - @Inject FastScrollUtils fastScrollUtils; - @Inject EditorUtils editorUtils; - @Inject SyncedFolderProvider syncedFolderProvider; - @Inject FileActionHandler fileActionHandler; - @Inject AppScanOptionalFeature appScanOptionalFeature; - @Inject ThumbnailGenerator thumbnailGenerator; - @Inject AvatarGenerator avatarGenerator; - - @Inject E2EEActionResolver e2eeActionResolver; - private E2EEDialogPresenter e2eeDialogPresenter; - private EncryptedFolderClickHandler clickHandler; - private FolderEncryption folderEncryption; - private FileFragment.ContainerActivity mContainerActivity; - - protected OCFile mFile; - protected OCFileListAdapter mAdapter; - protected boolean mOnlyFoldersClickable; - protected boolean mFileSelectable; - - protected boolean mHideFab = true; - protected ActionMode mActiveActionMode; - protected boolean mIsActionModeNew; - protected MultiChoiceModeListener mMultiChoiceModeListener; - - protected SearchType currentSearchType; - protected boolean searchFragment; - protected SearchEvent searchEvent; - private OCFileListSearchTask searchTask; - protected String mLimitToMimeType; - private FloatingActionButton mFabMain; - public static boolean isMultipleFileSelectedForCopyOrMove = false; - public final ParentFolderFinder parentFolderFinder = new ParentFolderFinder(); - private FileListLayoutManager fileListLayoutManager; - - private static final Intent scanIntentExternalApp = new Intent("org.fairscan.app.action.SCAN_TO_PDF"); - - @Inject DeviceInfo deviceInfo; - - protected enum MenuItemAddRemove { - DO_NOTHING, - REMOVE_SORT, - REMOVE_GRID_AND_SORT, - ADD_GRID_AND_SORT_WITH_SEARCH - } + @Inject + lateinit var clientFactory: ClientFactory - protected MenuItemAddRemove menuItemAddRemoveValue = MenuItemAddRemove.ADD_GRID_AND_SORT_WITH_SEARCH; + @Inject + lateinit var throttler: Throttler - private final List mOriginalMenuItems = new ArrayList<>(); + @Inject + lateinit var themeUtils: ThemeUtils - private static OCFileDepth fileDepth = OCFileDepth.Root; + @Inject + lateinit var arbitraryDataProvider: ArbitraryDataProvider - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setHasOptionsMenu(true); - mMultiChoiceModeListener = new MultiChoiceModeListener(); - fileListLayoutManager = new FileListLayoutManager(this, preferences); + @Inject + lateinit var backgroundJobManager: BackgroundJobManager - final Bundle state = savedInstanceState != null ? savedInstanceState : getArguments(); - setSearchArgs(state); - mFile = BundleExtensionsKt.getParcelableArgument(state, KEY_FILE, OCFile.class); - searchFragment = currentSearchType != null && isSearchEventSet(searchEvent); - folderEncryption = new FolderEncryption(this); - clickHandler = new EncryptedFolderClickHandler(this); - e2eeDialogPresenter = new E2EEDialogPresenter(this); - } + @Inject + lateinit var fastScrollUtils: FastScrollUtils - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - OCFileListFragmentExtensionsKt.listenEncryptionDialogResult(this); - } + @Inject + lateinit var editorUtils: EditorUtils - @Override - public void onResume() { - // Don't handle search events if we're coming back from back stack - // The fragment has already been properly restored in onCreate/onActivityCreated - if (mFile != null) { - super.onResume(); - return; - } + @Inject + lateinit var syncedFolderProvider: SyncedFolderProvider - final var activity = getActivity(); - if (activity == null) { - return; - } + @Inject + lateinit var fileActionHandler: FileActionHandler - final Intent intent = activity.getIntent(); - if (IntentExtensionsKt.getParcelableArgument(intent, SEARCH_EVENT, SearchEvent.class) != null) { - searchEvent = IntentExtensionsKt.getParcelableArgument(intent, SEARCH_EVENT, SearchEvent.class); - } + @Inject + lateinit var appScanOptionalFeature: AppScanOptionalFeature - if (isSearchEventSet(searchEvent)) { - handleSearchEvent(searchEvent); - } + @Inject + lateinit var thumbnailGenerator: ThumbnailGenerator - super.onResume(); - } + @Inject + lateinit var avatarGenerator: AvatarGenerator - @Override - public void onDestroyView() { - super.onDestroyView(); - if (getAdapter() != null) { - getAdapter().cleanup(); - } - } + @Inject + lateinit var e2eeActionResolver: E2EEActionResolver + + @Inject + lateinit var deviceInfo: DeviceInfo + + lateinit var e2eeDialogPresenter: E2EEDialogPresenter + private set + + lateinit var encryptedClickHandler: EncryptedFolderClickHandler + private set + + lateinit var folderEncryption: FolderEncryption + private set + + var containerActivity: ContainerActivity? = null + private set /** - * {@inheritDoc} + * Use this to query the [OCFile] that is currently being displayed by this fragment */ - @Override - public void onAttach(@NonNull Context context) { - super.onAttach(context); - Log_OC.i(TAG, "onAttach"); - try { - mContainerActivity = (FileFragment.ContainerActivity) context; - } catch (ClassCastException e) { - throw new IllegalArgumentException(context + " must implement " + - FileFragment.ContainerActivity.class.getSimpleName(), e); - } - try { - setOnRefreshListener((OnEnforceableRefreshListener) context); + var currentFile: OCFile? = null + protected set - } catch (ClassCastException e) { - throw new IllegalArgumentException(context + " must implement " + - OnEnforceableRefreshListener.class.getSimpleName(), e); - } - } + var adapter: OCFileListAdapter? = null + protected set - public FileFragment.ContainerActivity getContainerActivity() { - return mContainerActivity; - } + open val commonAdapter: CommonOCFileListAdapterInterface? + get() = adapter - public FolderEncryption getFolderEncryption() { - return folderEncryption; - } + var currentSearchType: SearchType = SearchType.NO_SEARCH + var isSearchFragment = false + var searchEvent: SearchEvent? = null + protected set - public EncryptedFolderClickHandler getEncryptedClickHandler() { - return clickHandler; - } + var hideFab = true + val parentFolderFinder = ParentFolderFinder() - public E2EEDialogPresenter getE2eeDialogPresenter() { - return e2eeDialogPresenter; - } + var fileListLayoutManager: FileListLayoutManager? = null + private set - public E2EEActionResolver getE2eeActionResolver() { - return e2eeActionResolver; - } + protected var menuItemAddRemoveValue = MenuItemAddRemove.ADD_GRID_AND_SORT_WITH_SEARCH - public void setSearchArgs(Bundle state) { - SearchType argSearchType = NO_SEARCH; - SearchEvent argSearchEvent = null; + private var onlyFoldersClickable = false + private var fileSelectable = false + private var limitToMimeType: String? = null + private var activeActionMode: ActionMode? = null + private var isActionModeNew = false + private var searchTask: OCFileListSearchTask? = null + private var fabMain: FloatingActionButton? = null + private val multiChoiceModeListener = MultiChoiceModeListener() + private val mainHandler = Handler(Looper.getMainLooper()) - if (state != null) { - argSearchType = BundleExtensionsKt.getParcelableArgument(state, KEY_CURRENT_SEARCH_TYPE, SearchType.class); - argSearchEvent = BundleExtensionsKt.getParcelableArgument(state, SEARCH_EVENT, SearchEvent.class); - } + private val activeSearchEvent: SearchEvent? + get() = searchEvent?.takeIf { it.isSet() } - currentSearchType = Objects.requireNonNullElse(argSearchType, NO_SEARCH); + private val capabilities: OCCapability + get() = requireContainerActivity().storageManager.getCapability(accountManager.user.accountName) - if (argSearchEvent != null) { - searchEvent = argSearchEvent; - } + protected enum class MenuItemAddRemove { + DO_NOTHING, + REMOVE_SORT, + REMOVE_GRID_AND_SORT, + ADD_GRID_AND_SORT_WITH_SEARCH + } - if (searchEvent != null && currentSearchType != NO_SEARCH) { - searchFragment = true; + override fun onAttach(context: Context) { + super.onAttach(context) + Log_OC.i(TAG, "onAttach") + containerActivity = requireNotNull(context as? ContainerActivity) { + "$context must implement ${ContainerActivity::class.java.simpleName}" + } + val refreshListener = requireNotNull(context as? OnEnforceableRefreshListener) { + "$context must implement ${OnEnforceableRefreshListener::class.java.simpleName}" } + setOnRefreshListener(refreshListener) } - /** - * {@inheritDoc} - */ - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - Log_OC.i(TAG, "onCreateView() start"); - View v = super.onCreateView(inflater, container, savedInstanceState); + @Suppress("DEPRECATION") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + fileListLayoutManager = FileListLayoutManager(this, preferences) - final Bundle state = savedInstanceState != null ? savedInstanceState : getArguments(); - setSearchArgs(state); + val state = savedInstanceState ?: arguments + setSearchArgs(state) + currentFile = state.getParcelableArgument(KEY_FILE, OCFile::class.java) + isSearchFragment = activeSearchEvent != null + folderEncryption = FolderEncryption(this) + encryptedClickHandler = EncryptedFolderClickHandler(this) + e2eeDialogPresenter = E2EEDialogPresenter(this) + } - boolean allowContextualActions = (state != null && state.getBoolean(ARG_ALLOW_CONTEXTUAL_ACTIONS, false)); - if (allowContextualActions) { - setChoiceModeAsMultipleModal(state); - } + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + Log_OC.i(TAG, "onCreateView() start") + val view = super.onCreateView(inflater, container, savedInstanceState) - mFabMain = requireActivity().findViewById(R.id.fab_main); + val state = savedInstanceState ?: arguments + setSearchArgs(state) - if (mFabMain != null) { - // is not available in FolderPickerActivity - viewThemeUtils.material.themeFAB(mFabMain); + if (state?.getBoolean(ARG_ALLOW_CONTEXTUAL_ACTIONS, false) == true) { + setChoiceModeAsMultipleModal(state) } - Log_OC.i(TAG, "onCreateView() end"); - return v; + fabMain = requireActivity().findViewById(R.id.fab_main)?.also { + viewThemeUtils.material.themeFAB(it) + } + + Log_OC.i(TAG, "onCreateView() end") + return view } - @Override - public void onDetach() { - setOnRefreshListener(null); - mContainerActivity = null; + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + listenEncryptionDialogResult() + } - if (searchTask != null) { - searchTask.cancel(); - } - super.onDetach(); + @Deprecated("Deprecated in Java") + @Suppress("DEPRECATION") + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + Log_OC.i(TAG, "onActivityCreated() start") + prepareOCFileList(savedInstanceState) + listDirectory(MainApp.isOnlyOnDevice()) } - @Override - public void onPause() { - super.onPause(); - if (mAdapter != null) { - mAdapter.cancelAllPendingTasks(); + override fun onViewStateRestored(savedInstanceState: Bundle?) { + super.onViewStateRestored(savedInstanceState) + savedInstanceState?.let { + searchEvent = it.getParcelableArgument(SEARCH_EVENT, SearchEvent::class.java) } + } + + override fun onStart() { + super.onStart() + EventBus.getDefault().register(this) + } - final var activity = getActivity(); - if (activity != null) { - activity.getIntent().removeExtra(OCFileListFragment.SEARCH_EVENT); + override fun onResume() { + // Don't handle search events if we're coming back from back stack + // The fragment has already been properly restored in onCreate/onActivityCreated + if (currentFile != null) { + super.onResume() + return } + + val activity = activity ?: return + activity.intent.getParcelableArgument(SEARCH_EVENT, SearchEvent::class.java)?.let { searchEvent = it } + activeSearchEvent?.let { handleSearchEvent(it) } + + super.onResume() } - /** - * {@inheritDoc} - */ - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - Log_OC.i(TAG, "onActivityCreated() start"); - prepareOCFileList(savedInstanceState); - listDirectory(MainApp.isOnlyOnDevice()); + override fun onPause() { + super.onPause() + adapter?.cancelAllPendingTasks() + activity?.intent?.removeExtra(SEARCH_EVENT) } - public void prepareOCFileList(Bundle savedInstanceState) { - if (savedInstanceState != null) { - mFile = BundleExtensionsKt.getParcelableArgument(savedInstanceState, KEY_FILE, OCFile.class); + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + currentFile.logFileSize(TAG) + outState.putParcelable(KEY_FILE, currentFile) + if (isSearchFragment) { + outState.putParcelable(KEY_CURRENT_SEARCH_TYPE, currentSearchType) + activeSearchEvent?.let { outState.putParcelable(SEARCH_EVENT, it) } } + multiChoiceModeListener.storeStateIn(outState) + } - Bundle args = getArguments(); - mOnlyFoldersClickable = args != null && args.getBoolean(ARG_ONLY_FOLDERS_CLICKABLE, false); - mFileSelectable = args != null && args.getBoolean(ARG_FILE_SELECTABLE, false); - mLimitToMimeType = args != null ? args.getString(ARG_MIMETYPE, "") : ""; + override fun onStop() { + EventBus.getDefault().unregister(this) + super.onStop() + } - setAdapter(args); + override fun onDestroyView() { + super.onDestroyView() + adapter?.cleanup() + } - mHideFab = args != null && args.getBoolean(ARG_HIDE_FAB, false); + override fun onDetach() { + setOnRefreshListener(null) + containerActivity = null + searchTask?.cancel() + super.onDetach() + } - if (mHideFab) { - setFabVisible(false); - } else { - if (mFile != null) { - setFabVisible(canCreateInside(mFile)); - } else { - setFabVisible(true); - } + fun setSearchArgs(state: Bundle?) { + currentSearchType = state?.getParcelableArgument(KEY_CURRENT_SEARCH_TYPE, SearchType::class.java) + ?: SearchType.NO_SEARCH + state?.getParcelableArgument(SEARCH_EVENT, SearchEvent::class.java)?.let { searchEvent = it } - registerFabListener(); + if (searchEvent != null && currentSearchType != SearchType.NO_SEARCH) { + isSearchFragment = true } + } - if (!searchFragment) { - // do not touch search event if previously searched - if (getArguments() == null) { - searchEvent = null; - } else { - searchEvent = BundleExtensionsKt.getParcelableArgument(getArguments(), SEARCH_EVENT, SearchEvent.class); - } + fun prepareOCFileList(savedInstanceState: Bundle?) { + if (savedInstanceState != null) { + currentFile = savedInstanceState.getParcelableArgument(KEY_FILE, OCFile::class.java) } - prepareCurrentSearch(searchEvent); - setEmptyView(searchEvent); - if (mSortButton != null) { - mSortButton.setOnClickListener(v -> SortOrderUi.showDialog(requireFragmentManager(), - preferences.getSortOrderByFolder(mFile))); - } + val args = arguments + onlyFoldersClickable = args?.getBoolean(ARG_ONLY_FOLDERS_CLICKABLE, false) == true + fileSelectable = args?.getBoolean(ARG_FILE_SELECTABLE, false) == true + limitToMimeType = args?.getString(ARG_MIMETYPE, "") ?: "" - if (mSwitchGridViewButton != null) { - mSwitchGridViewButton.setOnClickListener(v -> { - if (fileListLayoutManager != null) { - if (isGridEnabled()) { - fileListLayoutManager.setListAsPreferred(); - } else { - fileListLayoutManager.setGridAsPreferred(); - } - } - setLayoutSwitchButton(); - }); + setAdapter(args) + + hideFab = args?.getBoolean(ARG_HIDE_FAB, false) == true + if (hideFab) { + setFabVisible(false) + } else { + setFabVisible(currentFile?.let { canCreateInside(it) } ?: true) + registerFabListener() } - if (getActivity() instanceof FileDisplayActivity fda) { - fda.updateActionBarTitleAndHomeButton(fda.getCurrentDir()); + // do not touch search event if previously searched + if (!isSearchFragment) { + searchEvent = arguments.getParcelableArgument(SEARCH_EVENT, SearchEvent::class.java) } - } + prepareCurrentSearch(searchEvent) + setEmptyView(searchEvent) - private boolean isReadOnlyFolder(OCFile folder) { - if (folder == null || mContainerActivity == null) { - return false; + mSortButton?.setOnClickListener { + SortOrderUi.showDialog(parentFragmentManager, preferences.getSortOrderByFolder(currentFile)) } + mSwitchGridViewButton?.setOnClickListener { toggleGridAndListLayout() } - final var storageManager = mContainerActivity.getStorageManager(); - return storageManager != null && storageManager.isReadOnly(folder); + (activity as? FileDisplayActivity)?.let { it.updateActionBarTitleAndHomeButton(it.currentDir) } } - private boolean canCreateInside(OCFile folder) { - return folder.canCreateFileAndFolder() && !isReadOnlyFolder(folder); + private fun toggleGridAndListLayout() { + fileListLayoutManager?.let { + if (isGridEnabled) it.setListAsPreferred() else it.setGridAsPreferred() + } + setLayoutSwitchButton() } - protected void setAdapter(Bundle args) { - boolean hideItemOptions = args != null && args.getBoolean(ARG_HIDE_ITEM_OPTIONS, false); - boolean isGridViewPreferred = false; - if (fileListLayoutManager != null) { - isGridViewPreferred = fileListLayoutManager.isGridViewPreferred(); - } + protected open fun setAdapter(args: Bundle?) { + val hideItemOptions = args?.getBoolean(ARG_HIDE_ITEM_OPTIONS, false) == true + val isGridViewPreferred = fileListLayoutManager?.isGridViewPreferred() == true - mAdapter = new OCFileListAdapter( - getActivity(), - accountManager.getUser(), + adapter = OCFileListAdapter( + requireActivity(), + accountManager.user, preferences, syncedFolderProvider, - mContainerActivity, + requireContainerActivity(), this, hideItemOptions, isGridViewPreferred, viewThemeUtils, thumbnailGenerator, avatarGenerator - ); + ) - setRecyclerViewAdapter(mAdapter); + setRecyclerViewAdapter(adapter) + recyclerView?.let { fastScrollUtils.applyFastScroll(it) } + } - if (getRecyclerView() != null) { - fastScrollUtils.applyFastScroll(getRecyclerView()); - } + protected fun setChoiceModeAsMultipleModal(savedInstanceState: Bundle?) { + savedInstanceState?.let { multiChoiceModeListener.loadStateFrom(it) } + (requireActivity() as? FileActivity)?.addDrawerListener(multiChoiceModeListener) } - protected void prepareCurrentSearch(SearchEvent event) { - if (isSearchEventSet(event)) { - setCurrentSearchType(event); - prepareActionBarItems(event); + fun registerFabListener() { + val fileActivity = activity as? FileActivity + if (fileActivity == null) { + Log_OC.w(TAG, "activity is null cannot register fab listener") + return } + + val fab = fabMain + if (fab == null) { + Log_OC.w(TAG, "mFabMain is null cannot register fab listener") + return + } + + viewThemeUtils.material.themeFAB(fab) + fab.setOnClickListener { showCreateBottomSheet(fileActivity) } } - /** - * register listener on FAB. - */ - public void registerFabListener() { - if (!(getActivity() instanceof FileActivity fileActivity)) { - Log_OC.w(TAG, "activity is null cannot register fab listener"); - return; + private fun showCreateBottomSheet(fileActivity: FileActivity) { + val currentDir = currentFile + if (currentDir == null) { + Log_OC.w(TAG, "currentDir is null cannot open bottom sheet dialog") + return } - if (mFabMain == null) { - Log_OC.w(TAG, "mFabMain is null cannot register fab listener"); - return; + OCFileListBottomSheetDialog( + fileActivity, + this, + deviceInfo, + accountManager.user, + currentDir, + themeUtils, + viewThemeUtils, + editorUtils, + appScanOptionalFeature + ).run { + behavior.state = BottomSheetBehavior.STATE_EXPANDED + behavior.skipCollapsed = true + show() } + } + fun setFabVisible(visible: Boolean) { // is not available in FolderPickerActivity - viewThemeUtils.material.themeFAB(mFabMain); - mFabMain.setOnClickListener(v -> { - var currentDir = getCurrentFile(); - if (currentDir == null) { - Log_OC.w(TAG, "currentDir is null cannot open bottom sheet dialog"); - return; + val fab = fabMain ?: return + + val fileDisplayActivity = activity as? FileDisplayActivity + if (fileDisplayActivity != null && + (fileDisplayActivity.isAlbumsFragment || fileDisplayActivity.isAlbumItemsFragment) + ) { + fab.hide() + return + } + + val activity = activity ?: return + activity.runOnUiThread { + if (visible) { + fab.show() + viewThemeUtils.material.themeFAB(fab) + } else { + fab.hide() } - - final OCFileListBottomSheetDialog dialog = new OCFileListBottomSheetDialog(fileActivity, - this, - deviceInfo, - accountManager.getUser(), - currentDir, - themeUtils, - viewThemeUtils, - editorUtils, - appScanOptionalFeature); - - dialog.getBehavior().setState(BottomSheetBehavior.STATE_EXPANDED); - dialog.getBehavior().setSkipCollapsed(true); - dialog.show(); - }); - } - - @Override - public void createFolder(boolean encrypted) { + fab.slideHideBottomBehavior(visible) + } + } + + fun setFabEnabled(enabled: Boolean) { + val fab = fabMain ?: return + val activity = activity ?: return + activity.runOnUiThread { + fab.isEnabled = enabled + viewThemeUtils.material.themeFAB(fab) + } + } + + fun slideHideBottomBehaviourForBottomNavigationView(visible: Boolean) { + val drawerActivity = activity as? DrawerActivity ?: return + drawerActivity.bottomNavigationView.slideHideBottomBehavior(visible) + } + + private fun isReadOnlyFolder(folder: OCFile?): Boolean { + folder ?: return false + val storageManager = containerActivity?.storageManager ?: return false + return storageManager.isReadOnly(folder) + } + + private fun canCreateInside(folder: OCFile): Boolean = folder.canCreateFileAndFolder() && !isReadOnlyFolder(folder) + + override fun createFolder(encrypted: Boolean) { if (encrypted) { - clickHandler.onNewEncryptedFolder(); - return; + encryptedClickHandler.onNewEncryptedFolder() + return } - showCreateFolderDialog(false); + showCreateFolderDialog(false) } - public void showCreateFolderDialog(boolean encrypted) { - final var activity = getActivity(); + fun showCreateFolderDialog(encrypted: Boolean) { + val activity = activity if (activity == null) { - Log_OC.e(TAG, "activity is null, cannot create a folder"); - return; + Log_OC.e(TAG, "activity is null, cannot create a folder") + return } - CreateFolderDialogFragment.newInstance(mFile, encrypted) - .show(activity.getSupportFragmentManager(), DIALOG_CREATE_FOLDER); + CreateFolderDialogFragment.newInstance(currentFile, encrypted) + .show(activity.supportFragmentManager, DIALOG_CREATE_FOLDER) } - @Override - public void uploadFromApp() { - Intent action = new Intent(Intent.ACTION_GET_CONTENT); - action = action.setType("*/*").addCategory(Intent.CATEGORY_OPENABLE); - action.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); + @Suppress("DEPRECATION") + override fun uploadFromApp() { + val action = Intent(Intent.ACTION_GET_CONTENT) + .setType(ALL_MIME_TYPES) + .addCategory(Intent.CATEGORY_OPENABLE) + .putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) requireActivity().startActivityForResult( Intent.createChooser(action, getString(R.string.upload_chooser_title)), FileDisplayActivity.REQUEST_CODE__SELECT_CONTENT_FROM_APPS - ); + ) } - @Override - public void directCameraUpload() { - FileDisplayActivity fileDisplayActivity = (FileDisplayActivity) getActivity(); - + override fun directCameraUpload() { + val fileDisplayActivity = activity as? FileDisplayActivity if (fileDisplayActivity == null) { - SnackbarUtil.show(getView(), getString(R.string.error_starting_direct_camera_upload)); - return; + SnackbarUtil.show(view, getString(R.string.error_starting_direct_camera_upload)) + return } if (!PermissionUtil.checkSelfPermission(fileDisplayActivity, Manifest.permission.CAMERA)) { - PermissionUtil.requestCameraPermission(fileDisplayActivity, PermissionUtil.PERMISSIONS_CAMERA); - return; + PermissionUtil.requestCameraPermission(fileDisplayActivity, PermissionUtil.PERMISSIONS_CAMERA) + return } - showDirectCameraUploadAlertDialog(fileDisplayActivity); + showDirectCameraUploadAlertDialog(fileDisplayActivity) } - private void showDirectCameraUploadAlertDialog(FileDisplayActivity fileDisplayActivity) { - final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(fileDisplayActivity) + private fun showDirectCameraUploadAlertDialog(fileDisplayActivity: FileDisplayActivity) { + val builder = MaterialAlertDialogBuilder(fileDisplayActivity) .setTitle(R.string.upload_direct_camera_promt) .setIcon(R.mipmap.ic_launcher) - .setPositiveButton(R.string.upload_direct_camera_video, (dialog, which) -> fileDisplayActivity.getFileOperationsHelper().uploadFromCamera(fileDisplayActivity, FileDisplayActivity.REQUEST_CODE__UPLOAD_FROM_VIDEO_CAMERA, true)) - .setNegativeButton(R.string.upload_direct_camera_photo, (dialog, which) -> fileDisplayActivity.getFileOperationsHelper().uploadFromCamera(fileDisplayActivity, FileDisplayActivity.REQUEST_CODE__UPLOAD_FROM_CAMERA, false)); - - viewThemeUtils.dialog.colorMaterialAlertDialogBackground(fileDisplayActivity, builder); + .setPositiveButton(R.string.upload_direct_camera_video) { _, _ -> + fileDisplayActivity.fileOperationsHelper.uploadFromCamera( + fileDisplayActivity, + FileDisplayActivity.REQUEST_CODE__UPLOAD_FROM_VIDEO_CAMERA, + true + ) + } + .setNegativeButton(R.string.upload_direct_camera_photo) { _, _ -> + fileDisplayActivity.fileOperationsHelper.uploadFromCamera( + fileDisplayActivity, + FileDisplayActivity.REQUEST_CODE__UPLOAD_FROM_CAMERA, + false + ) + } - builder.create(); - builder.show(); + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(fileDisplayActivity, builder) + builder.show() } - @Override - public void scanDocUpload() { - FileDisplayActivity fileDisplayActivity = (FileDisplayActivity) getActivity(); - - final OCFile currentFile = getCurrentFile(); - if (fileDisplayActivity != null && currentFile != null && currentFile.isFolder()) { - - Intent intent = new Intent(requireContext(), DocumentScanActivity.class); - intent.putExtra(DocumentScanActivity.EXTRA_FOLDER, currentFile.getRemotePath()); - startActivity(intent); - } else { - Log.w(TAG, "scanDocUpload: Failed to start doc scanning, fileDisplayActivity=" + fileDisplayActivity + - ", currentFile=" + currentFile); - SnackbarUtil.show(this, R.string.error_starting_doc_scan); + override fun scanDocUpload() { + val fileDisplayActivity = activity as? FileDisplayActivity + val currentFile = currentFile + if (fileDisplayActivity == null || currentFile == null || !currentFile.isFolder) { + Log.w( + TAG, + "scanDocUpload: Failed to start doc scanning, fileDisplayActivity=$fileDisplayActivity, " + + "currentFile=$currentFile" + ) + SnackbarUtil.show(this, R.string.error_starting_doc_scan) + return } + + val intent = Intent(requireContext(), DocumentScanActivity::class.java) + .putExtra(DocumentScanActivity.EXTRA_FOLDER, currentFile.remotePath) + startActivity(intent) } - @Override - public void scanDocUploadFromApp() { + @Suppress("DEPRECATION") + override fun scanDocUploadFromApp() { requireActivity().startActivityForResult( scanIntentExternalApp, - FileDisplayActivity.REQUEST_CODE__SELECT_CONTENT_FROM_APPS_AUTO_RENAME); + FileDisplayActivity.REQUEST_CODE__SELECT_CONTENT_FROM_APPS_AUTO_RENAME + ) } - @Override - public boolean isScanDocUploadFromAppAvailable() { - var context = getActivity(); - if (context == null) { - return false; + override val isScanDocUploadFromAppAvailable: Boolean + get() { + val context = activity ?: return false + return scanIntentExternalApp.resolveActivity(context.packageManager) != null } - return scanIntentExternalApp.resolveActivity(context.getPackageManager()) != null; - } - @Override - public void uploadFiles() { - if (!(getActivity() instanceof FileActivity fileActivity)) { - Log_OC.w(TAG,"Activity is null, cant upload files"); - return; + override fun uploadFiles() { + val fileActivity = activity as? FileActivity + if (fileActivity == null) { + Log_OC.w(TAG, "Activity is null, cant upload files") + return } - final var user = fileActivity.getUser(); - if (user.isEmpty()) { - Log_OC.w(TAG,"User not exist, cant upload files"); - return; + val user = fileActivity.user.getOrNull() + if (user == null) { + Log_OC.w(TAG, "User not exist, cant upload files") + return } - final var file = getCurrentFile(); + val file = currentFile if (file == null) { - Log_OC.w(TAG,"File is null cannot determine isWithinEncryptedFolder, cant upload files"); - return; + Log_OC.w(TAG, "File is null cannot determine isWithinEncryptedFolder, cant upload files") + return } - boolean isWithinEncryptedFolder = file.isEncrypted(); - UploadFilesActivity.startUploadActivityForResult(fileActivity, user.get(), FileDisplayActivity.REQUEST_CODE__SELECT_FILES_FROM_FILE_SYSTEM, isWithinEncryptedFolder); + UploadFilesActivity.startUploadActivityForResult( + fileActivity, + user, + FileDisplayActivity.REQUEST_CODE__SELECT_FILES_FROM_FILE_SYSTEM, + file.isEncrypted + ) } - @Override - public void createRichWorkspace() { - if (!(getActivity() instanceof FileActivity fileActivity)) { - return; - } + override fun createRichWorkspace() { + val fileActivity = activity as? FileActivity ?: return + val remotePath = currentFile?.remotePath ?: return - fileActivity.getFilesRepository().createRichWorkspace(mFile.getRemotePath(), url -> { - mContainerActivity.getFileOperationsHelper().openRichWorkspaceWithTextEditor(mFile, url, requireContext()); - return Unit.INSTANCE; - }, () -> { - SnackbarUtil.show(getView(), R.string.failed_to_start_editor); - return Unit.INSTANCE; - }); + fileActivity.filesRepository.createRichWorkspace( + remotePath, + onCompleted = { url -> + containerActivity?.fileOperationsHelper + ?.openRichWorkspaceWithTextEditor(currentFile, url, requireContext()) + }, + onError = { SnackbarUtil.show(view, R.string.failed_to_start_editor) } + ) } - @Override - public void onShareIconClick(OCFile file) { - mContainerActivity.showDetails(file, 1); - } + override fun newDocument() = + showRichDocumentsTemplateDialog(ChooseRichDocumentsTemplateDialogFragment.Type.DOCUMENT) - @Override - public void showShareDetailView(OCFile file) { - mContainerActivity.showDetails(file, 1); - } + override fun newSpreadsheet() = + showRichDocumentsTemplateDialog(ChooseRichDocumentsTemplateDialogFragment.Type.SPREADSHEET) - @Override - public void showActivityDetailView(OCFile file) { - mContainerActivity.showDetails(file, 0); - } + override fun newPresentation() = + showRichDocumentsTemplateDialog(ChooseRichDocumentsTemplateDialogFragment.Type.PRESENTATION) - @Override - public void onOverflowIconClicked(OCFile file, View view) { - final Set checkedFiles = new HashSet<>(); - checkedFiles.add(file); - openActionsMenu(1, checkedFiles, true); + private fun showRichDocumentsTemplateDialog(type: ChooseRichDocumentsTemplateDialogFragment.Type) { + ChooseRichDocumentsTemplateDialogFragment.newInstance(currentFile, type) + .show(requireActivity().supportFragmentManager, DIALOG_CREATE_DOCUMENT) } - public void openActionsMenu(final int filesCount, final Set checkedFiles, final boolean isOverflow) { - throttler.run("overflowClick", () -> { - final var actionsToHide = FileAction.Companion.getFileListActionsToHide(checkedFiles); + override fun showTemplate(creator: Creator?, headline: String?) { + ChooseTemplateDialogFragment.newInstance(currentFile, creator, headline) + .show(requireActivity().supportFragmentManager, DIALOG_CREATE_DOCUMENT) + } - final var mimeType = MimeTypeExtensionsKt.resolveMimeType(checkedFiles.iterator().next()); - List endpoints = getCapabilities().getClientIntegrationEndpoints(Type.CONTEXT_MENU, mimeType); + override fun onShareIconClick(file: OCFile) { + containerActivity?.showDetails(file, SHARING_TAB) + } - final var childFragmentManager = getChildFragmentManager(); - final var actionBottomSheet = FileActionsBottomSheet.newInstance(filesCount, checkedFiles, isOverflow, actionsToHide, endpoints) - .setResultListener(childFragmentManager, this, (id) -> onFileActionChosen(id, checkedFiles)); + override fun showShareDetailView(file: OCFile) { + containerActivity?.showDetails(file, SHARING_TAB) + } - if (FragmentExtensionsKt.isDialogFragmentReady(this)) { - actionBottomSheet.show(childFragmentManager, "actions"); - } - }); + override fun showActivityDetailView(file: OCFile) { + containerActivity?.showDetails(file, ACTIVITY_TAB) } - public void openCurrentFolderActionsMenu() { - final OCFile currentFolder = getCurrentFile(); - if (currentFolder == null || currentFolder.isRootDirectory()) { - return; - } + override fun onOverflowIconClicked(file: OCFile, view: View?) { + openActionsMenu(SINGLE_SELECTION, hashSetOf(file), true) + } - final Set checkedFiles = new HashSet<>(); - checkedFiles.add(currentFolder); + override fun onHeaderClicked() { + val file = currentFile ?: return + if (file.richWorkspace.isNullOrEmpty()) return - throttler.run("overflowClick", () -> { - final var actionsToHide = FileAction.Companion.getCurrentFolderActionsToHide(currentFolder); - final var childFragmentManager = getChildFragmentManager(); - final var actionBottomSheet = FileActionsBottomSheet.newInstance(currentFolder, true, actionsToHide) - .setResultListener(childFragmentManager, this, (id) -> onFileActionChosen(id, checkedFiles)); + val adapter = adapter + if (adapter == null || adapter.isMultiSelect()) return - if (FragmentExtensionsKt.isDialogFragmentReady(this)) { - actionBottomSheet.show(childFragmentManager, "actions"); - } - }); + val fileDisplayActivity = containerActivity as? FileDisplayActivity ?: return + fileDisplayActivity.startRichWorkspacePreview(file) } - @Override - public void newDocument() { - ChooseRichDocumentsTemplateDialogFragment.newInstance(mFile, - ChooseRichDocumentsTemplateDialogFragment.Type.DOCUMENT) - .show(requireActivity().getSupportFragmentManager(), DIALOG_CREATE_DOCUMENT); - } + override fun onLongItemClicked(file: OCFile): Boolean { + val activity = activity ?: return true + + if (activeActionMode != null) { + toggleItemToCheckedList(file) + } else { + activity.startActionMode(multiChoiceModeListener) + commonAdapter?.addCheckedFile(file) + } + updateActionModeFile(file) - @Override - public void newSpreadsheet() { - ChooseRichDocumentsTemplateDialogFragment.newInstance(mFile, - ChooseRichDocumentsTemplateDialogFragment.Type.SPREADSHEET) - .show(requireActivity().getSupportFragmentManager(), DIALOG_CREATE_DOCUMENT); + return true } - @Override - public void newPresentation() { - ChooseRichDocumentsTemplateDialogFragment.newInstance(mFile, - ChooseRichDocumentsTemplateDialogFragment.Type.PRESENTATION) - .show(requireActivity().getSupportFragmentManager(), DIALOG_CREATE_DOCUMENT); + override fun onItemClicked(file: OCFile) { + onItemClicked(file, null) } - @Override - public void onHeaderClicked() { - final OCFile file = getCurrentFile(); + @OptIn(markerClass = [UnstableApi::class]) + override fun onItemClicked(file: OCFile?, sourceView: View?) { if (file == null) { - return; + Log_OC.d(TAG, "Null object in ListAdapter!") + return } - if (TextUtils.isEmpty(file.getRichWorkspace())) { - return; + val commonAdapter = commonAdapter + when { + commonAdapter?.isMultiSelect() == true -> toggleItemToCheckedList(file) + commonAdapter != null && file.isFolder -> folderOnItemClick(file, commonAdapter.getItemPosition(file)) + fileSelectable -> finishWithSelectedFile(file) + !onlyFoldersClickable -> fileOnItemClick(file, sourceView) } + } - final var adapter = getAdapter(); - if (adapter == null || adapter.isMultiSelect()) { - return; + override fun isLoading(): Boolean = false + + private fun finishWithSelectedFile(file: OCFile) { + val intent = Intent().putExtra(FolderPickerActivity.EXTRA_FILES, file) + requireActivity().run { + setResult(Activity.RESULT_OK, intent) + finish() } + } - if (!(mContainerActivity instanceof FileDisplayActivity fda)) { - return; + private fun folderOnItemClick(file: OCFile, position: Int) { + val folderPickerActivity = requireActivity() as? FolderPickerActivity + if (folderPickerActivity != null) { + val filenameErrorMessage = FileNameValidator.checkFileName(file.fileName, capabilities, requireContext()) + if (filenameErrorMessage != null) { + SnackbarUtil.show(folderPickerActivity, filenameErrorMessage) + return + } } - fda.startRichWorkspacePreview(file); - } + if (file.isEncrypted) { + encryptedClickHandler.onEncryptedFolderClick(file, position) + return + } - @Override - public void showTemplate(Creator creator, String headline) { - ChooseTemplateDialogFragment.newInstance(mFile, creator, headline).show(requireActivity() - .getSupportFragmentManager(), - DIALOG_CREATE_DOCUMENT); + isSearchFragment = false + setEmptyListMessage(EmptyListState.LOADING) + browseToFolder(file, position) } - /** - * Handler for multiple selection mode. - *

- * Manages input from the user when one or more files or folders are selected in the list. - *

- * Also listens to changes in navigation drawer to hide and recover multiple selection when it's opened and closed. - */ - private class MultiChoiceModeListener implements AbsListView.MultiChoiceModeListener, DrawerLayout.DrawerListener { + private fun fileOnItemClick(file: OCFile, sourceView: View?) { + val errorMessageId = getOpenRestrictionMessage(file) + val recyclerView = recyclerView + if (recyclerView != null && errorMessageId != null) { + Snackbar.make(recyclerView, errorMessageId, Snackbar.LENGTH_LONG).show() + return + } - private static final String KEY_ACTION_MODE_CLOSED_BY_DRAWER = "KILLED_ACTION_MODE"; + val fileDisplayActivity = containerActivity as? FileDisplayActivity + when { + fileDisplayActivity != null && fileDisplayActivity.canPreviewInMediaPager(file) -> + fileDisplayActivity.previewImageWithSearchContext(file, isSearchFragment, currentSearchType, sourceView) - /** - * True when action mode is finished because the drawer was opened - */ - private boolean mActionModeClosedByDrawer; + file.isDown && fileDisplayActivity != null -> + fileDisplayActivity.previewFile( + file, + object : CompletionCallback { + override fun onComplete(value: Boolean) = setFabVisible(value) + } + ) - /** - * Selected items in list when action mode is closed by drawer - */ - private final Set mSelectionWhenActionModeClosedByDrawer = new HashSet<>(); + else -> handlePendingDownloadFile(file) + } + } + + @StringRes + private fun getOpenRestrictionMessage(file: OCFile): Int? = when { + file.isAPKorAAB -> R.string.gplay_restriction + file.isOfflineOperation -> R.string.offline_operations_file_does_not_exists_yet + else -> null + } - @Override - public void onDrawerSlide(@NonNull View drawerView, float slideOffset) { - // nothing to do + private fun handlePendingDownloadFile(file: OCFile) { + if (!isAccountManagerInitialized()) { + Log_OC.e(TAG, "AccountManager not yet initialized") + return } - @Override - public void onDrawerOpened(@NonNull View drawerView) { - // nothing to do + val context = context + if (context == null) { + Log_OC.e(TAG, "context not available") + return } - /** - * When the navigation drawer is closed, action mode is recovered in the same state as was when the drawer was - * (started to be) opened. - * - * @param drawerView Navigation drawer just closed. - */ - @Override - public void onDrawerClosed(@NonNull View drawerView) { - if (!mActionModeClosedByDrawer || mSelectionWhenActionModeClosedByDrawer.isEmpty()) { - return; - } + val webViewAvailable = context.isWebViewAvailable() + val fileDisplayActivity = containerActivity as? FileDisplayActivity - FragmentActivity actionBarActivity = getActivity(); - if (actionBarActivity != null) { - actionBarActivity.startActionMode(mMultiChoiceModeListener); + when { + !file.isEncrypted && fileDisplayActivity != null && fileDisplayActivity.canPreviewInAudioPlayer(file) -> { + setFabVisible(false) + fileDisplayActivity.startAudioPreview(file, true, true) } - getAdapter().setCheckedItem(mSelectionWhenActionModeClosedByDrawer); + webViewAvailable && hasTextEditor(file) && !file.isEncrypted -> + TextEditorWebView.startTextEditor(file, context) - if (mActiveActionMode != null) { - mActiveActionMode.invalidate(); - } + supportsDirectEditing(file, webViewAvailable) -> + containerActivity?.fileOperationsHelper?.openFileAsRichDocument(file, context) - mSelectionWhenActionModeClosedByDrawer.clear(); - } + fileDisplayActivity != null -> { + fileDisplayActivity.startDownloadForPreview(file, currentFile) - /** - * If the action mode is active when the navigation drawer starts to move, the action mode is closed and the - * selection stored to be recovered when the drawer is closed. - * - * @param newState One of STATE_IDLE, STATE_DRAGGING or STATE_SETTLING. - */ - @Override - public void onDrawerStateChanged(int newState) { - if (DrawerLayout.STATE_DRAGGING != newState || mActiveActionMode == null) { - return; + // Checks if the file is small enough to be previewed immediately without showing progress. + // If the file is smaller than or equal to 1MB, it can be displayed directly. + if (file.isFileEligibleForImmediatePreview) { + fileDisplayActivity.setFileIDForImmediatePreview(file.fileId) + } } + } + } - if (getRecyclerView() != null && getRecyclerView().getAdapter() instanceof OCFileListAdapter fileListAdapter) { - mSelectionWhenActionModeClosedByDrawer.addAll(fileListAdapter.getCheckedItems()); - } + private fun hasTextEditor(file: OCFile): Boolean = editorUtils.getEditor(accountManager.user, file.mimeType) != null - mActiveActionMode.finish(); - mActionModeClosedByDrawer = true; - } + private fun supportsDirectEditing(file: OCFile, webViewAvailable: Boolean): Boolean { + if (!webViewAvailable) return false - /** - * Update action mode bar when an item is selected / unselected in the list - */ - @Override - public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { - // nothing to do here - } + val user = accountManager.user + if (user.isAnonymous) return false - /** - * Load menu and customize UI when action mode is started. - */ - @Override - @SuppressFBWarnings("RV") - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - mActiveActionMode = mode; - // Determine if actionMode is "new" or not (already affected by item-selection) - mIsActionModeNew = true; + val capability = containerActivity?.storageManager?.getCapability(user.accountName) ?: return false + val mimeTypes = capability.richDocumentsMimeTypeList ?: return false - // fake menu to be able to use bottom sheet instead - MenuInflater inflater = requireActivity().getMenuInflater(); - inflater.inflate(R.menu.custom_menu_placeholder, menu); + return file.mimeType in mimeTypes && + capability.richDocumentsDirectEditing.isTrue && + !file.isEncrypted + } - final MenuItem item = menu.findItem(R.id.custom_menu_placeholder_item); - if (item.getIcon() != null) { - item.setIcon(viewThemeUtils.platform.colorDrawable(item.getIcon(), ContextCompat.getColor(requireContext(), R.color.white))); - } + private fun toggleItemToCheckedList(file: OCFile) { + commonAdapter?.run { + if (isCheckedFile(file)) removeCheckedFile(file) else addCheckedFile(file) + } + updateActionModeFile(file) + } - mActiveActionMode.invalidate(); + private fun updateActionModeFile(file: OCFile) { + isActionModeNew = false + val actionMode = activeActionMode ?: return + actionMode.invalidate() + commonAdapter?.notifyItemChanged(file) + } - //set actionMode color - int statusBarColor = ContextCompat.getColor(requireContext(), R.color.action_mode_background); - viewThemeUtils.platform.colorStatusBar(requireActivity(), statusBarColor); - SystemBarBackgroundCallbacks.colorStatusBar(requireActivity(), statusBarColor); + @SuppressLint("NotifyDataSetChanged") + fun selectAllFiles(select: Boolean) { + val adapter = recyclerView?.adapter + if (adapter !is CommonOCFileListAdapterInterface) return - // hide FAB in multi selection mode - setFabVisible(false); + adapter.selectAll(select) + adapter.notifyDataSetChanged() + activeActionMode?.invalidate() + } - if (OCFileListFragment.this instanceof GalleryFragment && getActivity() instanceof AlbumsPickerActivity) { - item.setVisible(false); - final MenuItem addAlbumItem = menu.findItem(R.id.add_to_album); - // show add to album button when picking files from media to add to album - addAlbumItem.setVisible(true); - } + fun exitSelectionMode() { + activeActionMode?.finish() + } - final var adapter = getCommonAdapter(); - if (adapter != null) { - adapter.setMultiSelect(true); - } + private fun invalidateActionMode() { + activeActionMode?.invalidate() + } - return true; + fun openActionsMenu(filesCount: Int, checkedFiles: Set, isOverflow: Boolean) { + throttler.run(OVERFLOW_CLICK_THROTTLE_KEY) { + val actionsToHide = FileAction.getFileListActionsToHide(checkedFiles) + val mimeType = checkedFiles.first().resolveMimeType() + val endpoints = capabilities.getClientIntegrationEndpoints(Type.CONTEXT_MENU, mimeType) + val bottomSheet = FileActionsBottomSheet.newInstance( + filesCount, + checkedFiles, + isOverflow, + actionsToHide, + endpoints = endpoints + ) + showActionsBottomSheet(bottomSheet, checkedFiles) } + } + fun openCurrentFolderActionsMenu() { + val currentFolder = currentFile + if (currentFolder == null || currentFolder.isRootDirectory) return - /** - * Updates available action in menu depending on current selection. - */ - @Override - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - Set checkedFiles = getCommonAdapter().getCheckedItems(); - final int checkedCount = checkedFiles.size(); - - if (mActiveActionMode != null) { - String title = getResources().getQuantityString(R.plurals.items_selected_count, - checkedCount, - checkedCount); - mActiveActionMode.setTitle(title); - } + val checkedFiles = hashSetOf(currentFolder) + throttler.run(OVERFLOW_CLICK_THROTTLE_KEY) { + val actionsToHide = FileAction.getCurrentFolderActionsToHide(currentFolder) + val bottomSheet = FileActionsBottomSheet.newInstance(currentFolder, true, actionsToHide) + showActionsBottomSheet(bottomSheet, checkedFiles) + } + } - // Determine if we need to finish the action mode because there are no items selected - if (checkedCount == 0 && !mIsActionModeNew) { - exitSelectionMode(); - } + private fun showActionsBottomSheet(bottomSheet: FileActionsBottomSheet, checkedFiles: Set) { + bottomSheet.setResultListener(childFragmentManager, this) { id -> onFileActionChosen(id, checkedFiles) } + if (isDialogFragmentReady()) { + bottomSheet.show(childFragmentManager, ACTIONS_BOTTOM_SHEET_TAG) + } + } - isMultipleFileSelectedForCopyOrMove = (checkedCount > 0); + /** + * Start the appropriate action(s) on the currently selected files given menu selected by the user. + * + * @param checkedFiles List of files selected by the user on which the action should be performed + * @return 'true' if the menu selection started any action, 'false' otherwise. + */ + open fun onFileActionChosen(@IdRes itemId: Int, checkedFiles: Set): Boolean { + if (checkedFiles.isEmpty()) return false + if (onListActionChosen(itemId, checkedFiles)) return true - return true; + val containerActivity = containerActivity ?: return false + val host = FileActionHost(containerActivity, parentFragmentManager, view, currentFile, activeActionMode) + val isHandled = fileActionHandler.handle(itemId, checkedFiles, host) + if (isHandled && itemId in SELECTION_ENDING_ACTIONS) { + exitSelectionMode() } - /** - * Starts the corresponding action when a menu item is tapped by the user. - */ - @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - final Set checkedFiles = getCommonAdapter().getCheckedItems(); - if (item.getItemId() == R.id.custom_menu_placeholder_item) { - openActionsMenu(getCommonAdapter().getFilesCount(), checkedFiles, false); - } else if (item.getItemId() == R.id.add_to_album && OCFileListFragment.this instanceof GalleryFragment galleryFragment) { - galleryFragment.addImagesToAlbum(checkedFiles); - } - return true; - } + return isHandled + } - /** - * Restores UI. - */ - @Override - public void onDestroyActionMode(ActionMode mode) { - mActiveActionMode = null; - - // show FAB on multi selection mode exit - if (!mHideFab && !searchFragment) { - final var file = mFile; - if (file != null) { - setFabVisible(file.canCreateFileAndFolder()); - } - } + private fun onListActionChosen(@IdRes itemId: Int, checkedFiles: Set): Boolean { + when { + itemId == R.id.action_see_details && checkedFiles.size == SINGLE_SELECTION -> + showDetailsAndHideSortGroup(checkedFiles.first()) - final var activity = getActivity(); - if (activity != null) { - viewThemeUtils.platform.resetStatusBar(activity); - SystemBarBackgroundCallbacks.apply(activity, viewThemeUtils); + itemId == R.id.action_download_file || itemId == R.id.action_sync_file -> { + syncAndCheckFiles(checkedFiles) + exitSelectionMode() } - final var adapter = getCommonAdapter(); - if (adapter != null) { - adapter.setMultiSelect(false); - adapter.clearCheckedItems(); - } + itemId == R.id.action_move_or_copy -> moveOrCopy(checkedFiles) - isMultipleFileSelectedForCopyOrMove = false; - } + itemId == R.id.action_select_all_action_menu -> selectAllFiles(true) - public void storeStateIn(Bundle outState) { - outState.putBoolean(KEY_ACTION_MODE_CLOSED_BY_DRAWER, mActionModeClosedByDrawer); - } + itemId == R.id.action_deselect_all_action_menu -> selectAllFiles(false) - public void loadStateFrom(Bundle savedInstanceState) { - mActionModeClosedByDrawer = savedInstanceState.getBoolean(KEY_ACTION_MODE_CLOSED_BY_DRAWER, - mActionModeClosedByDrawer); - } - } + itemId == R.id.action_send_file -> containerActivity?.fileOperationsHelper?.sendFiles(checkedFiles) - /** - * Init listener that will handle interactions in multiple selection mode. - */ - protected void setChoiceModeAsMultipleModal(Bundle savedInstanceState) { - if (savedInstanceState != null) { - mMultiChoiceModeListener.loadStateFrom(savedInstanceState); + else -> return false } - ((FileActivity) requireActivity()).addDrawerListener(mMultiChoiceModeListener); + + return true } - /** - * Saves the current listed folder. - */ - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - FileExtensionsKt.logFileSize(mFile, TAG); - outState.putParcelable(KEY_FILE, mFile); - if (searchFragment) { - outState.putParcelable(KEY_CURRENT_SEARCH_TYPE, currentSearchType); - if (isSearchEventSet(searchEvent)) { - outState.putParcelable(OCFileListFragment.SEARCH_EVENT, searchEvent); - } + private fun showDetailsAndHideSortGroup(file: OCFile) { + activeActionMode?.finish() + containerActivity?.run { + showDetails(file) + showSortListGroup(false) } - mMultiChoiceModeListener.storeStateIn(outState); } - @Override - public void onPrepareOptionsMenu(@NonNull Menu menu) { - if (mOriginalMenuItems.isEmpty()) { - mOriginalMenuItems.add(menu.findItem(R.id.action_search)); + private fun moveOrCopy(checkedFiles: Set) { + val invalidFilename = findInvalidFilename(checkedFiles) + if (invalidFilename != null) { + SnackbarUtil.show( + requireActivity(), + getString(R.string.file_name_validator_rename_before_move_or_copy, invalidFilename) + ) + return } - if (menuItemAddRemoveValue == MenuItemAddRemove.REMOVE_GRID_AND_SORT) { - menu.removeItem(R.id.action_search); + if (!FileNameValidator.checkParentRemotePaths(checkedFiles.toList(), capabilities, requireContext())) { + browseToRoot() + SnackbarUtil.show(requireActivity(), R.string.file_name_validator_current_path_is_invalid) + return } - if (currentSearchType == FAVORITE_SEARCH) { - resetMenuItems(); - } else { - updateSortAndGridMenuItems(); - } + pickFolderForMoveOrCopy(checkedFiles) + } + + private fun findInvalidFilename(files: Set): String? = files.firstNotNullOfOrNull { + FileNameValidator.checkFileName(it.fileName, capabilities, requireContext()) } - private void updateSortAndGridMenuItems() { - if (mSwitchGridViewButton == null || mSortButton == null) { - return; + @Suppress("DEPRECATION") + private fun pickFolderForMoveOrCopy(checkedFiles: Set) { + val activity = requireActivity() + val paths = checkedFiles.mapTo(ArrayList(checkedFiles.size)) { it.remotePath } + val action = Intent(activity, FolderPickerActivity::class.java).apply { + putStringArrayListExtra(FolderPickerActivity.EXTRA_FILE_PATHS, paths) + putExtra(FolderPickerActivity.EXTRA_FOLDER, currentFile) + // No animation since we stay in the same folder + addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) + putExtra(FolderPickerActivity.EXTRA_ACTION, FolderPickerActivity.MOVE_OR_COPY) } + activity.startActivityForResult(action, FileDisplayActivity.REQUEST_CODE__MOVE_OR_COPY_FILES) + } - switch (menuItemAddRemoveValue) { - case ADD_GRID_AND_SORT_WITH_SEARCH: - mSwitchGridViewButton.setVisibility(View.VISIBLE); - mSortButton.setVisibility(View.VISIBLE); - break; + private fun syncAndCheckFiles(files: Collection) { + if (files.isEmpty()) return - case REMOVE_SORT: - mSortButton.setVisibility(View.GONE); - break; + (containerActivity as? FileActivity)?.showSyncLoadingDialog(files.any { it.isFolder }) - case REMOVE_GRID_AND_SORT: - mSortButton.setVisibility(View.GONE); - mSwitchGridViewButton.setVisibility(View.GONE); - break; + val lastIndex = files.size - 1 + files.forEachIndexed { index, file -> + if (FileStorageUtils.checkIfEnoughSpace(file)) { + containerActivity?.fileOperationsHelper?.syncFileOrFolder(file, index == lastIndex, false) + } else { + showSpaceErrorDialog(file, FileOperationsHelper.getAvailableSpaceOnDevice()) + } + } + } - case DO_NOTHING: - default: - Log_OC.v(TAG, "Kept the options menu default structure"); - break; + private fun showSpaceErrorDialog(file: OCFile, availableSpaceOnDevice: Long) { + parentFragmentManager.setFragmentResultListener( + SyncFileNotEnoughSpaceDialogFragment.REQUEST_KEY, + viewLifecycleOwner + ) { _, result -> + val resultFile = result.getParcelableArgument( + SyncFileNotEnoughSpaceDialogFragment.RESULT_FILE, + OCFile::class.java + ) + val action = result.getString(SyncFileNotEnoughSpaceDialogFragment.RESULT_ACTION) + if (action == SyncFileNotEnoughSpaceDialogFragment.ACTION_CHOOSE && resultFile != null) { + onItemClicked(resultFile) + } } + + SyncFileNotEnoughSpaceDialogFragment.newInstance(file, availableSpaceOnDevice) + .show(parentFragmentManager, ConfirmationDialogFragment.FTAG_CONFIRMATION) } /** * Call this, when the user presses the up button. - *

+ * * Tries to move up the current folder one level. If the parent folder was removed from the database, it continues * browsing up until finding an existing folders. - *

- * return Count of folder levels browsed up. + * + * @return Count of folder levels browsed up. */ - public int onBrowseUp() { - if (mFile == null || mFile.isRootDirectory()) { - return 0; - } - - final var result = parentFolderFinder.getParent(mFile, mContainerActivity.getStorageManager()); - OCFile target = result.getSecond(); + fun onBrowseUp(): Int { + val file = currentFile + if (file == null || file.isRootDirectory) return 0 + val (levelsUp, target) = parentFolderFinder.getParent(file, containerActivity?.storageManager) if (target == null) { - Log_OC.e(TAG, "onBrowseUp: could not resolve parent, staying put"); - return 0; + Log_OC.e(TAG, "onBrowseUp: could not resolve parent, staying put") + return 0 } - mFile = target; - setFileDepth(mFile); + currentFile = target + setFileDepth(target) - if (mFile.isRootDirectory() && currentSearchType != NO_SEARCH && isSearchEventSet(searchEvent)) { - searchFragment = true; + if (target.isRootDirectory && currentSearchType != SearchType.NO_SEARCH && activeSearchEvent != null) { + isSearchFragment = true } - updateFileList(); - return result.getFirst(); + updateFileList() + return levelsUp } - private void updateFileList() { - listDirectory(mFile, MainApp.isOnlyOnDevice()); - onRefresh(false); - restoreIndexAndTopPosition(); + private fun updateFileList() { + listDirectory(currentFile, MainApp.isOnlyOnDevice()) + onRefresh(false) + restoreIndexAndTopPosition() } - /** - * Will toggle a file selection status from the action mode - * - * @param file The concerned OCFile by the selection/deselection - */ - private void toggleItemToCheckedList(OCFile file) { - if (getCommonAdapter().isCheckedFile(file)) { - getCommonAdapter().removeCheckedFile(file); - } else { - getCommonAdapter().addCheckedFile(file); + fun browseToFolder(file: OCFile?, position: Int) { + setFileDepth(file) + + if (currentSearchType == SearchType.FAVORITE_SEARCH) { + resetMenuItems() } - updateActionModeFile(file); + + listDirectory(file, MainApp.isOnlyOnDevice()) + containerActivity?.onBrowsedDownTo(file) + saveIndexAndTopPosition(position) } - /** - * Will update (invalidate) the action mode adapter/mode to refresh an item selection change - * - * @param file The concerned OCFile to refresh in adapter - */ - private void updateActionModeFile(OCFile file) { - mIsActionModeNew = false; - if (mActiveActionMode != null) { - mActiveActionMode.invalidate(); - getCommonAdapter().notifyItemChanged(file); - } + private fun browseToRoot() { + val root = requireContainerActivity().storageManager.getFileByEncryptedRemotePath(OCFile.ROOT_PATH) + browseToFolder(root, FIRST_POSITION) } - @Override - public boolean onLongItemClicked(OCFile file) { - FragmentActivity actionBarActivity = getActivity(); - if (actionBarActivity != null) { - // Create only once instance of action mode - if (mActiveActionMode != null) { - toggleItemToCheckedList(file); - } else { - actionBarActivity.startActionMode(mMultiChoiceModeListener); - getCommonAdapter().addCheckedFile(file); - } - updateActionModeFile(file); - } + fun setFileDepth(file: OCFile?) { + currentFileDepth = file.getDepth() + } - return true; + fun resetFileDepth() { + currentFileDepth = OCFileDepth.Root } - private void folderOnItemClick(OCFile file, int position) { - if (requireActivity() instanceof FolderPickerActivity fpa) { - String filenameErrorMessage = FileNameValidator.INSTANCE.checkFileName(file.getFileName(), - getCapabilities(), - requireContext()); - if (filenameErrorMessage != null) { - SnackbarUtil.show(fpa, filenameErrorMessage); - return; - } - } + val fileDepth: OCFileDepth? + get() = currentFileDepth - if (file.isEncrypted()) { - clickHandler.onEncryptedFolderClick(file, position); - } else { - // update state and view of this fragment - searchFragment = false; - setEmptyListMessage(EmptyListState.LOADING); - browseToFolder(file, position); - } + /** + * Calls [OCFileListFragment.listDirectory] with a null parameter + */ + fun listDirectory(onlyOnDevice: Boolean) { + listDirectory(null, onlyOnDevice) } - private Integer checkFileBeforeOpen(OCFile file) { - if (file.isAPKorAAB()) { - return R.string.gplay_restriction; - } else if (file.isOfflineOperation()) { - return R.string.offline_operations_file_does_not_exists_yet; - } else { - return null; - } + fun listDirectory(directory: OCFile?, onlyOnDevice: Boolean) { + listDirectory(directory, null, onlyOnDevice) } - private void fileOnItemClick(OCFile file, @Nullable View sourceView) { - Integer errorMessageId = checkFileBeforeOpen(file); - final var recyclerView = getRecyclerView(); - if (recyclerView != null && errorMessageId != null) { - Snackbar.make(recyclerView, errorMessageId, Snackbar.LENGTH_LONG).show(); - return; + /** + * Lists the given directory on the view. When the input parameter is null, it will either refresh the last known + * directory. list the root if there never was a directory. + * + * @param directory File to be listed + */ + fun listDirectory(directory: OCFile?, file: OCFile?, onlyOnDevice: Boolean) { + if (isSearchFragment) { + activeSearchEvent?.let { + handleSearchEvent(it) + mRefreshListLayout?.isRefreshing = false + } + return } - if (mContainerActivity instanceof FileDisplayActivity fda && fda.canPreviewInMediaPager(file)) { - fda.previewImageWithSearchContext(file, searchFragment, currentSearchType, sourceView); - } else if (file.isDown() && mContainerActivity instanceof FileDisplayActivity fda) { - fda.previewFile(file, this::setFabVisible); - } else { - handlePendingDownloadFile(file); + val storageManager = containerActivity?.storageManager + if (storageManager == null) { + Log_OC.d(TAG, "fileDataStorageManager is null") + return + } + + val directoryToList = resolveDirectoryToList(directory, storageManager) + if (directoryToList == null) { + Log_OC.e(TAG, "directory is null, no files, wait for sync") + return } - } - private boolean supportsDirectEditing(OCFile file, boolean webViewAvailable) { - if (!webViewAvailable) { - return false; + val mimeType = limitToMimeType + if (mimeType == null) { + Log_OC.w(TAG, "mLimitToMimeType is null") + return } - final var account = accountManager.getUser(); - if (account.isAnonymous()) { - return false; + val adapter = adapter + if (adapter == null) { + Log_OC.e(TAG, "❗oc file list adapter is null, cannot list directory❗") + return } - final var capability = - mContainerActivity.getStorageManager().getCapability(account.getAccountName()); + Log_OC.i(TAG, "listing directory: " + directoryToList.decryptedRemotePath) + adapter.swapDirectory(accountManager.user, directoryToList, storageManager, onlyOnDevice, mimeType) - final var mimeTypes = capability.getRichDocumentsMimeTypeList(); - return mimeTypes != null - && mimeTypes.contains(file.getMimeType()) - && capability.getRichDocumentsDirectEditing().isTrue() - && !file.isEncrypted(); + val previousDirectory = currentFile + currentFile = directoryToList + updateLayout() + scrollAfterListing(adapter, file, previousDirectory, directoryToList) } - private void handlePendingDownloadFile(OCFile file) { - if (!isAccountManagerInitialized()) { - Log_OC.e(TAG, "AccountManager not yet initialized"); - return; - } + private fun scrollAfterListing( + adapter: OCFileListAdapter, + highlightedFile: OCFile?, + previousDirectory: OCFile?, + listedDirectory: OCFile + ) { + val recyclerView = recyclerView - final var context = getContext(); - if (context == null) { - Log_OC.e(TAG, "context not available"); - return; - } - - boolean webViewAvailable = ContextExtensionsKt.isWebViewAvailable(context); - - if (!file.isEncrypted() && mContainerActivity instanceof FileDisplayActivity fda && fda.canPreviewInAudioPlayer(file)) { - setFabVisible(false); - fda.startAudioPreview(file, true, true); - } else if (webViewAvailable && editorUtils.getEditor(accountManager.getUser(), file.getMimeType()) != null && !file.isEncrypted()) { - TextEditorWebView.Companion.startTextEditor(file, context); - } else if (supportsDirectEditing(file, webViewAvailable)) { - mContainerActivity.getFileOperationsHelper().openFileAsRichDocument(file, context); - } else if (mContainerActivity instanceof FileDisplayActivity fda) { - fda.startDownloadForPreview(file, mFile); - - // Checks if the file is small enough to be previewed immediately without showing progress. - // If the file is smaller than or equal to 1MB, it can be displayed directly. - if (file.isFileEligibleForImmediatePreview()) { - fda.setFileIDForImmediatePreview(file.getFileId()); + if (highlightedFile != null) { + adapter.setHighlightedItem(highlightedFile) + val position = adapter.getItemPosition(highlightedFile) + if (position != RecyclerView.NO_POSITION && recyclerView != null) { + recyclerView.scrollToPosition(position) } + return } - } - @Override - public void onItemClicked(OCFile file) { - onItemClicked(file, null); + if (recyclerView != null && previousDirectory != listedDirectory) { + recyclerView.scrollToPosition(FIRST_POSITION) + } } - @Override - @OptIn(markerClass = UnstableApi.class) - public void onItemClicked(OCFile file, @Nullable View sourceView) { - if (getCommonAdapter() != null && getCommonAdapter().isMultiSelect()) { - toggleItemToCheckedList(file); - } else { - if (file == null) { - Log_OC.d(TAG, "Null object in ListAdapter!"); - return; - } + private fun resolveDirectoryToList(directory: OCFile?, storageManager: FileDataStorageManager): OCFile? { + val candidate = directory ?: currentFile ?: storageManager.getFileByPath(OCFile.ROOT_PATH) ?: return null + if (candidate.isFolder) return candidate - if (getCommonAdapter() != null && file.isFolder()) { - int position = getCommonAdapter().getItemPosition(file); - folderOnItemClick(file, position); - } else if (mFileSelectable) { - Intent intent = new Intent(); - intent.putExtra(FolderPickerActivity.EXTRA_FILES, file); - requireActivity().setResult(Activity.RESULT_OK, intent); - requireActivity().finish(); - } else if (!mOnlyFoldersClickable) { - fileOnItemClick(file, sourceView); - } - } + Log_OC.w(TAG, "You see, that is not a directory -> $candidate") + return storageManager.getFileById(candidate.parentId) } - public void setFileDepth(OCFile file) { - fileDepth = OCFileExtensionsKt.getDepth(file); + fun refreshDirectory() { + isSearchFragment = false + val currentFile = currentFile ?: return + setFabVisible(canCreateInside(currentFile)) + listDirectory(currentFile, MainApp.isOnlyOnDevice()) } - public void resetFileDepth() { - fileDepth = OCFileDepth.Root; - } + val adapterFiles: List + get() = adapter?.files.orEmpty() - public OCFileDepth getFileDepth() { - return fileDepth; + fun updateOCFile(file: OCFile) { + adapter?.replaceFile(file) } - protected void browseToFolder(OCFile file, int position) { - setFileDepth(file); + val isEmpty: Boolean + get() = adapter?.isEmpty ?: true - if (currentSearchType == FAVORITE_SEARCH) { - resetMenuItems(); - } + private fun updateLayout() { + fileListLayoutManager?.setLayoutViewMode() + updateSortButton() + setLayoutSwitchButton() - listDirectory(file, MainApp.isOnlyOnDevice()); - // then, notify parent activity to let it update its state and view - mContainerActivity.onBrowsedDownTo(file); - // save index and top position - saveIndexAndTopPosition(position); + val file = currentFile + val readOnly = isReadOnlyFolder(file) + setFabVisible(!hideFab && !readOnly) + slideHideBottomBehaviourForBottomNavigationView(!hideFab) + setFabEnabled(file != null && !readOnly && (file.canCreateFileAndFolder() || file.isOfflineOperation)) + + invalidateActionMode() } - /** - * Start the appropriate action(s) on the currently selected files given menu selected by the user. - * - * @param checkedFiles List of files selected by the user on which the action should be performed - * @return 'true' if the menu selection started any action, 'false' otherwise. - */ - public boolean onFileActionChosen(@IdRes final int itemId, Set checkedFiles) { - if (checkedFiles.isEmpty()) { - return false; + private fun updateSortButton() { + val sortButton = mSortButton ?: return + val sortOrder = if (currentSearchType == SearchType.FAVORITE_SEARCH) { + preferences.getSortOrderByType(FileSortOrder.Type.favoritesListView, FileSortOrder.SORT_A_TO_Z) + } else { + preferences.getSortOrderByFolder(currentFile) } + sortButton.setText(SortOrderUi.labelRes(sortOrder)) + } - if (onListActionChosen(itemId, checkedFiles)) { - return true; + @Deprecated("Deprecated in Java") + override fun onPrepareOptionsMenu(menu: Menu) { + if (menuItemAddRemoveValue == MenuItemAddRemove.REMOVE_GRID_AND_SORT) { + menu.removeItem(R.id.action_search) } - final var host = new FileActionHost(mContainerActivity, - getParentFragmentManager(), - getView(), - mFile, - mActiveActionMode); - final boolean isHandled = fileActionHandler.handle(itemId, checkedFiles, host); - if (isHandled && SELECTION_ENDING_ACTIONS.contains(itemId)) { - exitSelectionMode(); + if (currentSearchType == SearchType.FAVORITE_SEARCH) { + resetMenuItems() + } else { + updateSortAndGridMenuItems() } - - return isHandled; } - private boolean onListActionChosen(@IdRes final int itemId, Set checkedFiles) { - if (itemId == R.id.action_see_details && checkedFiles.size() == SINGLE_SELECTION) { - if (mActiveActionMode != null) { - mActiveActionMode.finish(); - } - - mContainerActivity.showDetails(checkedFiles.iterator().next()); - mContainerActivity.showSortListGroup(false); - return true; - } else if (itemId == R.id.action_download_file || itemId == R.id.action_sync_file) { - syncAndCheckFiles(checkedFiles); - exitSelectionMode(); - return true; - } else if (itemId == R.id.action_move_or_copy) { - String invalidFilename = checkInvalidFilenames(checkedFiles); - - if (invalidFilename != null) { - SnackbarUtil.show(requireActivity(), getString(R.string.file_name_validator_rename_before_move_or_copy, invalidFilename)); - return true; - } + private fun updateSortAndGridMenuItems() { + val switchGridViewButton = mSwitchGridViewButton ?: return + val sortButton = mSortButton ?: return - if (!FileNameValidator.INSTANCE.checkParentRemotePaths(new ArrayList<>(checkedFiles), getCapabilities(), requireContext())) { - browseToRoot(); - SnackbarUtil.show(requireActivity(), R.string.file_name_validator_current_path_is_invalid); - return true; + when (menuItemAddRemoveValue) { + MenuItemAddRemove.ADD_GRID_AND_SORT_WITH_SEARCH -> { + switchGridViewButton.visibility = View.VISIBLE + sortButton.visibility = View.VISIBLE } - pickFolderForMoveOrCopy(checkedFiles); - return true; - } else if (itemId == R.id.action_select_all_action_menu) { - selectAllFiles(true); - return true; - } else if (itemId == R.id.action_deselect_all_action_menu) { - selectAllFiles(false); - return true; - } else if (itemId == R.id.action_send_file) { - mContainerActivity.getFileOperationsHelper().sendFiles(checkedFiles); - return true; - } + MenuItemAddRemove.REMOVE_SORT -> sortButton.visibility = View.GONE - return false; - } - - private void browseToRoot() { - OCFile root = mContainerActivity.getStorageManager().getFileByEncryptedRemotePath(ROOT_PATH); - browseToFolder(root,0); - } - - private OCCapability getCapabilities() { - final User currentUser = accountManager.getUser(); - return mContainerActivity.getStorageManager().getCapability(currentUser.getAccountName()); - } - - private String checkInvalidFilenames(Set checkedFiles) { - for (OCFile file : checkedFiles) { - String errorMessage = FileNameValidator.INSTANCE.checkFileName(file.getFileName(), getCapabilities(), requireContext()); - if (errorMessage != null) { - return errorMessage; + MenuItemAddRemove.REMOVE_GRID_AND_SORT -> { + sortButton.visibility = View.GONE + switchGridViewButton.visibility = View.GONE } - } - - return null; - } - private void pickFolderForMoveOrCopy(final Set checkedFiles) { - int requestCode = FileDisplayActivity.REQUEST_CODE__MOVE_OR_COPY_FILES; - String extraAction = FolderPickerActivity.MOVE_OR_COPY; - - final Intent action = new Intent(getActivity(), FolderPickerActivity.class); - final ArrayList paths = new ArrayList<>(checkedFiles.size()); - for (OCFile file : checkedFiles) { - paths.add(file.getRemotePath()); + MenuItemAddRemove.DO_NOTHING -> Log_OC.v(TAG, "Kept the options menu default structure") } - action.putStringArrayListExtra(FolderPickerActivity.EXTRA_FILE_PATHS, paths); - action.putExtra(FolderPickerActivity.EXTRA_FOLDER, getCurrentFile()); - action.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); // No animation since we stay in the same folder - action.putExtra(FolderPickerActivity.EXTRA_ACTION, extraAction); - requireActivity().startActivityForResult(action, requestCode); } + private fun resetMenuItems() { + menuItemAddRemoveValue = MenuItemAddRemove.ADD_GRID_AND_SORT_WITH_SEARCH + updateSortAndGridMenuItems() + } /** - * Use this to query the {@link OCFile} that is currently being displayed by this fragment + * Returns the navigation drawer menu item corresponding to this fragment. * - * @return The currently viewed OCFile + * OCFileListFragment is the parent for GalleryFragment, SharedListFragment, + * and GroupfolderListFragment. It also internally handles listing favorites, + * shared files, or recently modified items via search events. This method + * checks the current fragment type and search state to give correct drawer menu ID. */ - @Nullable - public OCFile getCurrentFile() { - return mFile; - } + val menuItemId: Int + get() = when { + javaClass == GalleryFragment::class.java -> R.id.nav_gallery - /** - * Calls {@link OCFileListFragment#listDirectory(OCFile, boolean)} with a null parameter - */ - public void listDirectory(boolean onlyOnDevice) { - listDirectory(null, onlyOnDevice); - } + javaClass == SharedListFragment::class.java || + isSearchEventShared || + currentSearchType == SearchType.SHARED_FILTER -> R.id.nav_shared - public void refreshDirectory() { - searchFragment = false; + javaClass == GroupfolderListFragment::class.java || + currentSearchType == SearchType.GROUPFOLDER -> R.id.nav_groupfolders - if (mFile != null) { - setFabVisible(canCreateInside(mFile)); - } + isSearchEventFavorite || currentSearchType == SearchType.FAVORITE_SEARCH -> R.id.nav_favorites - final var currentFile = getCurrentFile(); - if (currentFile != null) { - listDirectory(currentFile, MainApp.isOnlyOnDevice()); + currentSearchType == SearchType.RECENT_FILES_SEARCH -> R.id.nav_recent_files + + else -> R.id.nav_all_files } + + protected fun prepareCurrentSearch(event: SearchEvent?) { + if (event == null || !event.isSet()) return + setCurrentSearchType(event) + prepareActionBarItems(event) } - public void listDirectory(@Nullable OCFile directory, boolean onlyOnDevice) { - listDirectory(directory, null, onlyOnDevice); + fun setCurrentSearchType(event: SearchEvent) { + event.toSearchType()?.let { currentSearchType = it } } - private OCFile getDirectoryForListDirectory(@Nullable OCFile directory, FileDataStorageManager storageManager) { - if (directory == null) { - if (mFile != null) { - directory = mFile; - } else { - directory = storageManager.getFileByPath(ROOT_PATH); - } + protected fun prepareActionBarItems(event: SearchEvent?) { + val searchType = event?.searchType + if (searchType == RemoteSearchType.FAVORITE_SEARCH || searchType == RemoteSearchType.RECENTLY_MODIFIED_SEARCH) { + menuItemAddRemoveValue = MenuItemAddRemove.REMOVE_SORT } - // If that's not a directory -> List its parent - if (!directory.isFolder()) { - Log_OC.w(TAG, "You see, that is not a directory -> " + directory); - directory = storageManager.getFileById(directory.getParentId()); + if (currentSearchType != SearchType.FILE_SEARCH) { + activity?.invalidateOptionsMenu() } - - return directory; } - /** - * Lists the given directory on the view. When the input parameter is null, it will either refresh the last known - * directory. list the root if there never was a directory. - * - * @param directory File to be listed - */ - public void listDirectory(@Nullable OCFile directory, OCFile file, boolean onlyOnDevice) { - if (!searchFragment) { - FileDataStorageManager storageManager = mContainerActivity.getStorageManager(); - if (storageManager == null) { - Log_OC.d(TAG, "fileDataStorageManager is null"); - return; - } - - directory = getDirectoryForListDirectory(directory, storageManager); - if (directory == null) { - Log_OC.e(TAG, "directory is null, no files, wait for sync"); - return; - } - - if (mLimitToMimeType == null) { - Log_OC.w(TAG, "mLimitToMimeType is null"); - return; - } - - if (mAdapter == null) { - Log_OC.e(TAG, "❗" + "oc file list adapter is null, cannot list directory" + "❗"); - return; - } - - Log_OC.i(TAG, "listing directory: " + directory.getDecryptedRemotePath()); - - mAdapter.swapDirectory( - accountManager.getUser(), - directory, - storageManager, - onlyOnDevice, - mLimitToMimeType); - - OCFile previousDirectory = mFile; - mFile = directory; - - updateLayout(); - - final var recyclerView = getRecyclerView(); - if (file != null) { - mAdapter.setHighlightedItem(file); - int position = mAdapter.getItemPosition(file); - if (position != -1 && recyclerView != null) { - recyclerView.scrollToPosition(position); - } - } else if (recyclerView != null && (previousDirectory == null || !previousDirectory.equals(directory))) { - recyclerView.scrollToPosition(0); - } - } else if (isSearchEventSet(searchEvent)) { - handleSearchEvent(searchEvent); - if (mRefreshListLayout != null) { - mRefreshListLayout.setRefreshing(false); - } + protected fun setEmptyView(event: SearchEvent?) { + val emptyListType = when (event?.searchType) { + RemoteSearchType.FILE_SEARCH -> SearchType.FILE_SEARCH + RemoteSearchType.FAVORITE_SEARCH -> SearchType.FAVORITE_SEARCH + RemoteSearchType.RECENTLY_MODIFIED_SEARCH -> SearchType.RECENT_FILES_SEARCH + RemoteSearchType.SHARED_FILTER -> SearchType.SHARED_FILTER + else -> SearchType.NO_SEARCH } + setEmptyListMessage(emptyListType) } - public List getAdapterFiles() { - return mAdapter.getFiles(); + fun resetSearchAttributes() { + isSearchFragment = false + searchEvent = null + currentSearchType = SearchType.NO_SEARCH } - public void updateOCFile(@NonNull OCFile file) { - mAdapter.replaceFile(file); - } + protected fun handleSearchEvent(event: SearchEvent) { + if (event.searchType == RemoteSearchType.PHOTO_SEARCH) return - private void updateLayout() { - if (fileListLayoutManager != null) { - fileListLayoutManager.setLayoutViewMode(); + if (searchTask?.isFinished() == false) { + searchEvent?.let { + Log_OC.d( + TAG, + "OCFileListSearchTask already running skipping new api call for search event: " + + it.searchType + ) + } + return } - updateSortButton(); - setLayoutSwitchButton(); - boolean readOnly = isReadOnlyFolder(mFile); - setFabVisible(!mHideFab && !readOnly); - slideHideBottomBehaviourForBottomNavigationView(!mHideFab); - setFabEnabled(mFile != null && !readOnly && (mFile.canCreateFileAndFolder() || mFile.isOfflineOperation())); - - invalidateActionMode(); - } - - private void updateSortButton() { - if (mSortButton != null) { - FileSortOrder sortOrder; - if (currentSearchType == FAVORITE_SEARCH) { - sortOrder = preferences.getSortOrderByType(FileSortOrder.Type.favoritesListView, FileSortOrder.SORT_A_TO_Z); - } else { - sortOrder = preferences.getSortOrderByFolder(mFile); + if (searchTask == null) { + activity?.runOnUiThread { + adapter?.removeAllFiles() + setEmptyListMessage(EmptyListState.LOADING) } - - mSortButton.setText(SortOrderUi.labelRes(sortOrder)); } - } - private void invalidateActionMode() { - if (mActiveActionMode != null) { - mActiveActionMode.invalidate(); - } - } + prepareCurrentSearch(event) + isSearchFragment = true + setFabVisible(false) - public CommonOCFileListAdapterInterface getCommonAdapter() { - return mAdapter; - } + mainHandler.post { + updateSortButton() + fileListLayoutManager?.setLayoutViewMode() + } - public OCFileListAdapter getAdapter() { - return mAdapter; + startSearchTask(event) } - public void setCurrentSearchType(SearchEvent event) { - final var searchType = event.toSearchType(); - if (searchType != null) { - currentSearchType = searchType; + private fun startSearchTask(event: SearchEvent) { + val currentUser = accountManager.user + val remoteOperation = if (currentSearchType == SearchType.RECENT_FILES_SEARCH) { + createRecentFilesSearchRemoteOperation() + } else { + getSearchRemoteOperation(currentUser, event) } - } - public void setCurrentSearchType(SearchType searchType) { - currentSearchType = searchType; + val storageManager = requireContainerActivity().storageManager + ?: FileDataStorageManager(currentUser, requireContext().contentResolver) + + @Suppress("UNCHECKED_CAST") + searchTask = OCFileListSearchTask( + this, + remoteOperation as RemoteOperation>, + currentUser, + event, + SharedListFragment.TASK_TIMEOUT.toLong(), + preferences, + storageManager + ).also { it.execute() } } - public SearchType getCurrentSearchType() { - return currentSearchType; + protected open fun getSearchRemoteOperation(currentUser: User, event: SearchEvent): RemoteOperation<*> { + val searchOnlyFolders = arguments?.getBoolean(ARG_SEARCH_ONLY_FOLDER, false) == true + val capability = requireContainerActivity().storageManager.getCapability(currentUser.accountName) + return SearchRemoteOperation(event.searchQuery, event.searchType, searchOnlyFolders, capability) } - protected void prepareActionBarItems(SearchEvent event) { - if (event != null) { - switch (event.getSearchType()) { - case FAVORITE_SEARCH: - case RECENTLY_MODIFIED_SEARCH: - menuItemAddRemoveValue = MenuItemAddRemove.REMOVE_SORT; - break; + private fun createRecentFilesSearchRemoteOperation(): RemoteOperation<*> { + val capability = capabilities + val nowSeconds = System.currentTimeMillis() / AppConstants.MILLIS_PER_SECOND - default: - // do nothing - break; - } + return SearchRemoteOperation("", RemoteSearchType.RECENTLY_MODIFIED_SEARCH, false, capability).apply { + startDate = nowSeconds - TimeUnit.DAYS.toSeconds(RECENT_FILES_MAX_AGE_DAYS) + endDate = nowSeconds + limit = RECENT_FILES_SEARCH_LIMIT } + } - if (FILE_SEARCH != currentSearchType) { - final var activity = getActivity(); - if (activity != null) { - activity.invalidateOptionsMenu(); - } + override fun onRefresh() { + val event = activeSearchEvent + if (isSearchFragment && event != null) { + handleSearchEvent(event) + mRefreshListLayout?.isRefreshing = false + return } + + isSearchFragment = false + super.onRefresh() } - protected void setEmptyView(SearchEvent event) { - if (event != null) { - switch (event.getSearchType()) { - case FILE_SEARCH: - setEmptyListMessage(FILE_SEARCH); - break; + fun cancelAndRetriggerSearch() { + searchTask?.cancel() + searchTask = null + searchEvent?.let { handleSearchEvent(it) } + } - case FAVORITE_SEARCH: - setEmptyListMessage(FAVORITE_SEARCH); - break; + val isSearchEventFavorite: Boolean + get() = searchEvent?.searchType == RemoteSearchType.FAVORITE_SEARCH - case RECENTLY_MODIFIED_SEARCH: - setEmptyListMessage(RECENT_FILES_SEARCH); - break; + val isSearchEventShared: Boolean + get() = searchEvent?.searchType == RemoteSearchType.SHARED_FILTER - case SHARED_FILTER: - setEmptyListMessage(SHARED_FILTER); - break; + fun shouldNavigateBackToAllFiles(): Boolean = + this is GalleryFragment || isSearchEventFavorite || isSearchEventShared - default: - setEmptyListMessage(NO_SEARCH); - break; - } - } else { - setEmptyListMessage(NO_SEARCH); - } - } + private fun SearchEvent.isSet(): Boolean = searchQuery.isNotEmpty() || searchType in SEARCH_TYPES_WITHOUT_QUERY @Subscribe(threadMode = ThreadMode.MAIN) - public void onMessageEvent(ChangeMenuEvent changeMenuEvent) { - Log_OC.d(TAG, "event bus --- change menu event triggered"); - - final var arguments = getArguments(); - if (arguments != null) { - arguments.clear(); - } - resetSearchAttributes(); - resetMenuItems(); + open fun onMessageEvent(changeMenuEvent: ChangeMenuEvent) { + Log_OC.d(TAG, "event bus --- change menu event triggered") - if (getActivity() instanceof FileDisplayActivity fda) { - fda.invalidateOptionsMenu(); - fda.getIntent().removeExtra(OCFileListFragment.SEARCH_EVENT); - fda.setupHomeSearchToolbarWithSortAndListButtons(); - fda.updateActionBarTitleAndHomeButton(null); - } + arguments?.clear() + resetSearchAttributes() + resetMenuItems() - if (mFile != null) { - setFabVisible(canCreateInside(mFile)); + (activity as? FileDisplayActivity)?.run { + invalidateOptionsMenu() + intent.removeExtra(SEARCH_EVENT) + setupHomeSearchToolbarWithSortAndListButtons() + updateActionBarTitleAndHomeButton(null) } - slideHideBottomBehaviourForBottomNavigationView(true); - } - - private void resetMenuItems() { - menuItemAddRemoveValue = MenuItemAddRemove.ADD_GRID_AND_SORT_WITH_SEARCH; - updateSortAndGridMenuItems(); - } - - public void resetSearchAttributes() { - searchFragment = false; - searchEvent = null; - currentSearchType = NO_SEARCH; + currentFile?.let { setFabVisible(canCreateInside(it)) } + slideHideBottomBehaviourForBottomNavigationView(true) } @Subscribe(threadMode = ThreadMode.BACKGROUND) - public void onMessageEvent(CommentsEvent event) { - mAdapter.markCommentsAsRead(event.getRemoteId()); + fun onMessageEvent(event: CommentsEvent) { + adapter?.markCommentsAsRead(event.remoteId) } @Subscribe(threadMode = ThreadMode.BACKGROUND) - public void onMessageEvent(FavoriteEvent event) { + fun onMessageEvent(event: FavoriteEvent) { try { - User user = accountManager.getUser(); - OwnCloudClient client = clientFactory.create(user); - - ToggleFavoriteRemoteOperation toggleFavoriteOperation = new ToggleFavoriteRemoteOperation( - event.getShouldFavorite(), event.getRemotePath()); - RemoteOperationResult remoteOperationResult = toggleFavoriteOperation.execute(client); - - if (remoteOperationResult.isSuccess()) { - boolean removeFromList = currentSearchType == FAVORITE_SEARCH && !event.getShouldFavorite(); - setEmptyListMessage(FAVORITE_SEARCH); - if (this instanceof GalleryFragment galleryFragment) { - galleryFragment.markAsFavorite(event.getRemotePath(), event.getShouldFavorite()); - } else { - mAdapter.updateFavoriteState(event.getRemotePath(), event.getShouldFavorite(), removeFromList); - } + val client = clientFactory.create(accountManager.user) + val result = ToggleFavoriteRemoteOperation(event.shouldFavorite, event.remotePath).execute(client) + if (!result.isSuccess) return + + val removeFromList = currentSearchType == SearchType.FAVORITE_SEARCH && !event.shouldFavorite + setEmptyListMessage(SearchType.FAVORITE_SEARCH) + if (this is GalleryFragment) { + markAsFavorite(event.remotePath, event.shouldFavorite) + } else { + adapter?.updateFavoriteState(event.remotePath, event.shouldFavorite, removeFromList) } - - } catch (ClientFactory.CreationException e) { - Log_OC.e(TAG, "Error processing event", e); - } - } - - @Override - public void onViewStateRestored(@Nullable Bundle savedInstanceState) { - super.onViewStateRestored(savedInstanceState); - - if (savedInstanceState != null) { - searchEvent = BundleExtensionsKt.getParcelableArgument(savedInstanceState, SEARCH_EVENT, SearchEvent.class); + } catch (e: ClientFactory.CreationException) { + Log_OC.e(TAG, "Error processing event", e) } } @Subscribe(threadMode = ThreadMode.BACKGROUND) - public void onMessageEvent(final SearchEvent event) { - handleSearchEvent(event); + fun onMessageEvent(event: SearchEvent) { + handleSearchEvent(event) } - protected void handleSearchEvent(SearchEvent event) { - if (SearchRemoteOperation.SearchType.PHOTO_SEARCH == event.getSearchType()) { - return; - } - - // avoid calling api multiple times if task is already executing - if (searchTask != null && !searchTask.isFinished()) { - if (searchEvent != null) { - Log_OC.d(TAG, "OCFileListSearchTask already running skipping new api call for search event: " + searchEvent.getSearchType()); - } - - return; - } - - final var activity = getActivity(); - - // only show loading state first time if app doesn't have active search task - if (activity != null && searchTask == null) { - activity.runOnUiThread(() -> { - getAdapter().removeAllFiles(); - setEmptyListMessage(EmptyListState.LOADING); - }); - } + @Subscribe(threadMode = ThreadMode.MAIN) + fun onMessageEvent(event: EncryptionEvent) { + event.onResult(this) + } - prepareCurrentSearch(event); - searchFragment = true; - setFabVisible(false); + @Subscribe(threadMode = ThreadMode.BACKGROUND) + fun onMessageEvent(event: FileLockEvent) { + val user = accountManager.user - new Handler(Looper.getMainLooper()).post(() -> { - updateSortButton(); - if (fileListLayoutManager != null) { - fileListLayoutManager.setLayoutViewMode(); + try { + val client = clientFactory.createNextcloudClient(user) + val result = ToggleFileLockRemoteOperation(event.shouldLock, event.filePath).execute(client) + if (result.isSuccess) { + // TODO only refresh the modified file? + mainHandler.post { onRefresh() } + return } - }); - - final User currentUser = accountManager.getUser(); - RemoteOperation remoteOperation; - if (currentSearchType == RECENT_FILES_SEARCH) { - remoteOperation = getRecentFilesSearchRemoteOperation(); - } else { - remoteOperation = getSearchRemoteOperation(currentUser, event); - } - - var storageManager = mContainerActivity.getStorageManager(); - if (storageManager == null) { - storageManager = new FileDataStorageManager(currentUser, requireContext().getContentResolver()); + } catch (e: ClientFactory.CreationException) { + Log_OC.e(TAG, "Cannot create client", e) } - searchTask = new OCFileListSearchTask(this, - remoteOperation, - currentUser, event, - SharedListFragment.TASK_TIMEOUT, - preferences, - storageManager); - searchTask.execute(); + showFileLockError() } - protected RemoteOperation getSearchRemoteOperation(final User currentUser, final SearchEvent event) { - boolean searchOnlyFolders = (getArguments() != null && getArguments().getBoolean(ARG_SEARCH_ONLY_FOLDER, false)); - - OCCapability ocCapability = mContainerActivity.getStorageManager() - .getCapability(currentUser.getAccountName()); - - return new SearchRemoteOperation(event.getSearchQuery(), - event.getSearchType(), - searchOnlyFolders, - ocCapability); + private fun showFileLockError() { + val recyclerView = recyclerView ?: return + Snackbar.make(recyclerView, R.string.error_file_lock, Snackbar.LENGTH_LONG).show() } - private RemoteOperation getRecentFilesSearchRemoteOperation() { - String accountName = accountManager.getUser().getAccountName(); - OCCapability capability = mContainerActivity.getStorageManager().getCapability(accountName); - String searchQuery = ""; + fun requireContainerActivity(): ContainerActivity = + checkNotNull(containerActivity) { "Fragment $this is not attached to a ContainerActivity" } - SearchRemoteOperation remoteOperation = new SearchRemoteOperation( - searchQuery, - SearchRemoteOperation.SearchType.RECENTLY_MODIFIED_SEARCH, - false, - capability - ); + /** + * Handler for multiple selection mode. + * + * Manages input from the user when one or more files or folders are selected in the list. + * + * Also listens to changes in navigation drawer to hide and recover multiple selection when it's opened and closed. + */ + private inner class MultiChoiceModeListener : + AbsListView.MultiChoiceModeListener, + DrawerLayout.DrawerListener { - long nowSeconds = System.currentTimeMillis() / AppConstants.MILLIS_PER_SECOND; - long last14DaysTimestamp = nowSeconds - TimeUnit.DAYS.toSeconds(14); + private var actionModeClosedByDrawer = false + private val selectionWhenActionModeClosedByDrawer = HashSet() - remoteOperation.setStartDate(last14DaysTimestamp); - remoteOperation.setEndDate(nowSeconds); - remoteOperation.setLimit(100); + override fun onDrawerSlide(drawerView: View, slideOffset: Float) = Unit - return remoteOperation; - } + override fun onDrawerOpened(drawerView: View) = Unit - @Subscribe(threadMode = ThreadMode.MAIN) - public void onMessageEvent(EncryptionEvent event) { - event.onResult(this); - } + /** + * When the navigation drawer is closed, action mode is recovered in the same state as was when the drawer was + * (started to be) opened. + */ + override fun onDrawerClosed(drawerView: View) { + if (!actionModeClosedByDrawer || selectionWhenActionModeClosedByDrawer.isEmpty()) return - @Subscribe(threadMode = ThreadMode.BACKGROUND) - public void onMessageEvent(FileLockEvent event) { - final User user = accountManager.getUser(); + activity?.startActionMode(this) + adapter?.setCheckedItem(selectionWhenActionModeClosedByDrawer) + activeActionMode?.invalidate() + selectionWhenActionModeClosedByDrawer.clear() + } - try { - NextcloudClient client = clientFactory.createNextcloudClient(user); - ToggleFileLockRemoteOperation operation = new ToggleFileLockRemoteOperation(event.getShouldLock(), event.getFilePath()); - RemoteOperationResult result = operation.execute(client); + /** + * If the action mode is active when the navigation drawer starts to move, the action mode is closed and the + * selection stored to be recovered when the drawer is closed. + */ + override fun onDrawerStateChanged(newState: Int) { + val actionMode = activeActionMode + if (newState != DrawerLayout.STATE_DRAGGING || actionMode == null) return - if (result.isSuccess()) { - // TODO only refresh the modified file? - new Handler(Looper.getMainLooper()).post(this::onRefresh); - } else { - final var recyclerView = getRecyclerView(); - if (recyclerView == null) { - return; - } - Snackbar.make(recyclerView, R.string.error_file_lock, Snackbar.LENGTH_LONG).show(); + (recyclerView?.adapter as? OCFileListAdapter)?.let { + selectionWhenActionModeClosedByDrawer.addAll(it.getCheckedItems()) } - } catch (ClientFactory.CreationException e) { - Log_OC.e(TAG, "Cannot create client", e); - final var recyclerView = getRecyclerView(); - if (recyclerView == null) { - return; - } - Snackbar.make(recyclerView, R.string.error_file_lock, Snackbar.LENGTH_LONG).show(); + actionMode.finish() + actionModeClosedByDrawer = true } - } - - @Override - public void onStart() { - super.onStart(); - EventBus.getDefault().register(this); - } - - @Override - public void onStop() { - EventBus.getDefault().unregister(this); - super.onStop(); - } + override fun onItemCheckedStateChanged(mode: ActionMode?, position: Int, id: Long, checked: Boolean) = Unit - @Override - public void onRefresh() { - if (searchFragment && isSearchEventSet(searchEvent)) { - handleSearchEvent(searchEvent); + @SuppressFBWarnings("RV") + override fun onCreateActionMode(mode: ActionMode?, menu: Menu): Boolean { + activeActionMode = mode + isActionModeNew = true - if (mRefreshListLayout != null) { - mRefreshListLayout.setRefreshing(false); + // fake menu to be able to use bottom sheet instead + requireActivity().menuInflater.inflate(R.menu.custom_menu_placeholder, menu) + val placeholderItem = menu.findItem(R.id.custom_menu_placeholder_item) + placeholderItem.icon?.let { + placeholderItem.icon = viewThemeUtils.platform.colorDrawable( + it, + ContextCompat.getColor(requireContext(), R.color.white) + ) } - } else { - searchFragment = false; - super.onRefresh(); - } - } - public void setSearchFragment(boolean searchFragment) { - this.searchFragment = searchFragment; - } + mode?.invalidate() + colorStatusBarForActionMode() + setFabVisible(false) - public boolean isSearchFragment() { - return searchFragment; - } + if (this@OCFileListFragment is GalleryFragment && activity is AlbumsPickerActivity) { + placeholderItem.isVisible = false + menu.findItem(R.id.add_to_album).isVisible = true + } - public void cancelAndRetriggerSearch() { - if (searchTask != null) { - searchTask.cancel(); - searchTask = null; - } - if (searchEvent != null) { - handleSearchEvent(searchEvent); + commonAdapter?.setMultiSelect(true) + return true } - } - /** - * De-/select all elements in the current list view. - * - * @param select true to select all, false to deselect all - */ - @SuppressLint("NotifyDataSetChanged") - public void selectAllFiles(boolean select) { - final var recyclerView = getRecyclerView(); - if (recyclerView == null) { - return; + private fun colorStatusBarForActionMode() { + val activity = requireActivity() + val statusBarColor = ContextCompat.getColor(requireContext(), R.color.action_mode_background) + viewThemeUtils.platform.colorStatusBar(activity, statusBarColor) + SystemBarBackgroundCallbacks.colorStatusBar(activity, statusBarColor) } - final var adapter = recyclerView.getAdapter(); - if (adapter instanceof CommonOCFileListAdapterInterface commonInterface) { - commonInterface.selectAll(select); - adapter.notifyDataSetChanged(); - mActiveActionMode.invalidate(); - } - } + override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean { + val checkedCount = commonAdapter?.getCheckedItems()?.size ?: 0 - /** - * Exits the multi file selection mode. - */ - public void exitSelectionMode() { - if (mActiveActionMode != null) { - mActiveActionMode.finish(); - } - } + activeActionMode?.title = resources.getQuantityString( + R.plurals.items_selected_count, + checkedCount, + checkedCount + ) - private boolean isSearchEventSet(SearchEvent event) { - if (event == null) { - return false; + if (checkedCount == 0 && !isActionModeNew) { + exitSelectionMode() + } + + isMultipleFileSelectedForCopyOrMove = checkedCount > 0 + return true } - SearchRemoteOperation.SearchType searchType = event.getSearchType(); - return !TextUtils.isEmpty(event.getSearchQuery()) || - searchType == SearchRemoteOperation.SearchType.SHARED_FILTER || - searchType == SearchRemoteOperation.SearchType.FAVORITE_SEARCH || - searchType == SearchRemoteOperation.SearchType.RECENTLY_MODIFIED_SEARCH; - } - private void syncAndCheckFiles(Collection files) { - if (files.isEmpty()) return; + override fun onActionItemClicked(mode: ActionMode?, item: MenuItem): Boolean { + val commonAdapter = commonAdapter ?: return true + val checkedFiles = commonAdapter.getCheckedItems() + + when (item.itemId) { + R.id.custom_menu_placeholder_item -> + openActionsMenu(commonAdapter.getFilesCount(), checkedFiles, false) - boolean hasFolder = files.stream().anyMatch(OCFile::isFolder); + R.id.add_to_album -> + (this@OCFileListFragment as? GalleryFragment)?.addImagesToAlbum(checkedFiles) + } - if (mContainerActivity instanceof FileActivity activity) { - activity.showSyncLoadingDialog(hasFolder); + return true } - List fileList = new ArrayList<>(files); - for (int i = 0; i < fileList.size(); i++) { - OCFile file = fileList.get(i); - boolean isLast = i == fileList.size() - 1; + override fun onDestroyActionMode(mode: ActionMode?) { + activeActionMode = null - if (FileStorageUtils.checkIfEnoughSpace(file)) { - mContainerActivity.getFileOperationsHelper().syncFileOrFolder(file, isLast, false); - } else { - showSpaceErrorDialog(file, FileOperationsHelper.getAvailableSpaceOnDevice()); + if (!hideFab && !isSearchFragment) { + currentFile?.let { setFabVisible(it.canCreateFileAndFolder()) } } - } - } - - private void showSpaceErrorDialog(OCFile file, long availableSpaceOnDevice) { - getParentFragmentManager().setFragmentResultListener( - SyncFileNotEnoughSpaceDialogFragment.REQUEST_KEY, - getViewLifecycleOwner(), - (requestKey, result) -> { - OCFile resultFile = result.getParcelable(SyncFileNotEnoughSpaceDialogFragment.RESULT_FILE); - String action = result.getString(SyncFileNotEnoughSpaceDialogFragment.RESULT_ACTION); - if (SyncFileNotEnoughSpaceDialogFragment.ACTION_CHOOSE.equals(action) && resultFile != null) { - this.onItemClicked(resultFile); - } - }); - SyncFileNotEnoughSpaceDialogFragment - .newInstance(file, availableSpaceOnDevice) - .show(getParentFragmentManager(), ConfirmationDialogFragment.FTAG_CONFIRMATION); - } + activity?.let { + viewThemeUtils.platform.resetStatusBar(it) + SystemBarBackgroundCallbacks.apply(it, viewThemeUtils) + } - @Override - public boolean isLoading() { - return false; - } + commonAdapter?.run { + setMultiSelect(false) + clearCheckedItems() + } - /** - * Sets the 'visibility' state of the FAB contained in the fragment. - *

- * When 'false' is set, FAB visibility is set to View.GONE programmatically. - * - * @param visible Desired visibility for the FAB. - */ - public void setFabVisible(final boolean visible) { - if (mFabMain == null) { - // is not available in FolderPickerActivity - return; + isMultipleFileSelectedForCopyOrMove = false } - // to hide the fab if user is on Albums Fragment - if (getActivity() instanceof FileDisplayActivity fda - && (fda.isAlbumsFragment() - || fda.isAlbumItemsFragment())) { - mFabMain.hide(); - return; + fun storeStateIn(outState: Bundle) { + outState.putBoolean(KEY_ACTION_MODE_CLOSED_BY_DRAWER, actionModeClosedByDrawer) } - final var activity = getActivity(); - if (activity == null) { - return; + fun loadStateFrom(savedInstanceState: Bundle) { + actionModeClosedByDrawer = savedInstanceState.getBoolean( + KEY_ACTION_MODE_CLOSED_BY_DRAWER, + actionModeClosedByDrawer + ) } + } - activity.runOnUiThread(() -> { - if (visible) { - mFabMain.show(); - viewThemeUtils.material.themeFAB(mFabMain); - } else { - mFabMain.hide(); - } + companion object { + val TAG: String = OCFileListFragment::class.java.simpleName - ViewExtensionsKt.slideHideBottomBehavior(mFabMain, visible); - }); - } + private val MY_PACKAGE = OCFileListFragment::class.java.`package`?.name ?: "com.owncloud.android.ui.fragment" - public void slideHideBottomBehaviourForBottomNavigationView(boolean visible) { - if (getActivity() instanceof DrawerActivity drawerActivity) { - ViewExtensionsKt.slideHideBottomBehavior(drawerActivity.getBottomNavigationView(), visible); - } - } + val ARG_ONLY_FOLDERS_CLICKABLE = "$MY_PACKAGE.ONLY_FOLDERS_CLICKABLE" + val ARG_FILE_SELECTABLE = "$MY_PACKAGE.FILE_SELECTABLE" + val ARG_ALLOW_CONTEXTUAL_ACTIONS = "$MY_PACKAGE.ALLOW_CONTEXTUAL" + val ARG_HIDE_FAB = "$MY_PACKAGE.HIDE_FAB" + val ARG_HIDE_ITEM_OPTIONS = "$MY_PACKAGE.HIDE_ITEM_OPTIONS" + val ARG_SEARCH_ONLY_FOLDER = "$MY_PACKAGE.SEARCH_ONLY_FOLDER" + val ARG_MIMETYPE = "$MY_PACKAGE.MIMETYPE" - /** - * Sets the 'visibility' state of the FAB contained in the fragment. - *

- * When 'false' is set, FAB is greyed out - * - * @param enabled Desired visibility for the FAB. - */ - public void setFabEnabled(final boolean enabled) { - final var fabMain = mFabMain; - if (fabMain == null) { - // is not available in FolderPickerActivity - return; - } + const val DOWNLOAD_SEND = "DOWNLOAD_SEND" - final var activity = getActivity(); - if (activity == null) { - return; - } + const val FOLDER_LAYOUT_LIST = "LIST" + const val FOLDER_LAYOUT_GRID = "GRID" - activity.runOnUiThread(() -> { - if (enabled) { - fabMain.setEnabled(true); - viewThemeUtils.material.themeFAB(fabMain); - } else { - fabMain.setEnabled(false); - viewThemeUtils.material.themeFAB(fabMain); - } - }); - } + const val SEARCH_EVENT = "SEARCH_EVENT" + const val KEY_CURRENT_SEARCH_TYPE = "CURRENT_SEARCH_TYPE" + private val KEY_FILE = "$MY_PACKAGE.extra.FILE" + private const val KEY_ACTION_MODE_CLOSED_BY_DRAWER = "KILLED_ACTION_MODE" - /** - * Returns the navigation drawer menu item corresponding to this fragment. - * - *

- * OCFileListFragment is the parent for GalleryFragment, SharedListFragment, - * and GroupfolderListFragment. It also internally handles listing favorites, - * shared files, or recently modified items via search events. This method - * checks the current fragment type and search state to give correct drawer menu ID. - *

- * - * @return the menu item ID to highlight in the navigation drawer - */ - public int getMenuItemId() { - if (getClass() == GalleryFragment.class) { - return R.id.nav_gallery; - } else if (getClass() == SharedListFragment.class || isSearchEventShared() || currentSearchType == SHARED_FILTER) { - return R.id.nav_shared; - } else if (getClass() == GroupfolderListFragment.class || currentSearchType == SearchType.GROUPFOLDER) { - return R.id.nav_groupfolders; - } else if (isSearchEventFavorite() || currentSearchType == FAVORITE_SEARCH) { - return R.id.nav_favorites; - } else if (currentSearchType == RECENT_FILES_SEARCH) { - return R.id.nav_recent_files; - } else { - return R.id.nav_all_files; - } - } + private const val DIALOG_CREATE_FOLDER = "DIALOG_CREATE_FOLDER" + private const val DIALOG_CREATE_DOCUMENT = "DIALOG_CREATE_DOCUMENT" + private const val ACTIONS_BOTTOM_SHEET_TAG = "actions" + private const val OVERFLOW_CLICK_THROTTLE_KEY = "overflowClick" - public boolean isEmpty() { - return mAdapter == null || mAdapter.isEmpty(); - } + private const val ALL_MIME_TYPES = "*/*" + private const val SCAN_TO_PDF_ACTION = "org.fairscan.app.action.SCAN_TO_PDF" - public SearchEvent getSearchEvent() { - return searchEvent; - } + private const val ACTIVITY_TAB = 0 + private const val SHARING_TAB = 1 + private const val FIRST_POSITION = 0 + private const val SINGLE_SELECTION = 1 - public boolean isSearchEventFavorite() { - return isSearchEvent(SearchRemoteOperation.SearchType.FAVORITE_SEARCH); - } + private const val RECENT_FILES_MAX_AGE_DAYS = 14L + private const val RECENT_FILES_SEARCH_LIMIT = 100 - public boolean isSearchEventShared() { - return isSearchEvent(SearchRemoteOperation.SearchType.SHARED_FILTER); - } + private val SELECTION_ENDING_ACTIONS = setOf( + R.id.action_export_file, + R.id.action_favorite, + R.id.action_unset_favorite, + R.id.action_add_to_album + ) - private boolean isSearchEvent(SearchRemoteOperation.SearchType givenEvent) { - if (searchEvent == null) { - return false; - } - return searchEvent.getSearchType() == givenEvent; - } + private val SEARCH_TYPES_WITHOUT_QUERY = setOf( + RemoteSearchType.SHARED_FILTER, + RemoteSearchType.FAVORITE_SEARCH, + RemoteSearchType.RECENTLY_MODIFIED_SEARCH + ) - public boolean shouldNavigateBackToAllFiles() { - return this instanceof GalleryFragment || - isSearchEventFavorite() || - isSearchEventShared(); - } + private val scanIntentExternalApp = Intent(SCAN_TO_PDF_ACTION) + + private var currentFileDepth: OCFileDepth? = OCFileDepth.Root - public FileListLayoutManager getFileListLayoutManager() { - return fileListLayoutManager; + var isMultipleFileSelectedForCopyOrMove = false } } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt index 0d98f5c01705..15ce56246bf3 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchTask.kt @@ -84,7 +84,7 @@ class OCFileListSearchTask( return@launch } - fragment.adapter.prepareForSearchData(fragment.currentSearchType) + fragment.adapter?.prepareForSearchData(fragment.currentSearchType) val remoteFiles = fetchAndSortRemoteFiles(searchType, cachedFiles, resultData, fragment) updateAdapterData(fragment, remoteFiles) } @@ -158,12 +158,12 @@ class OCFileListSearchTask( private suspend fun updateAdapterData(fragment: OCFileListFragment, newList: List) = withContext(Dispatchers.Main) { - if (!fragment.isAdded || !fragment.searchFragment) { + if (!fragment.isAdded || !fragment.isSearchFragment) { Log_OC.e(TAG, "cannot update adapter data, fragment is not ready") return@withContext } - fragment.adapter.updateAdapter(newList, null) + fragment.adapter?.updateAdapter(newList, null) } private fun sortSearchData(list: List, searchType: SearchType, fragment: OCFileListFragment): List { @@ -190,7 +190,7 @@ class OCFileListSearchTask( } } - fragment.adapter.setDisplayedSortOrder(sortOrder) + fragment.adapter?.setDisplayedSortOrder(sortOrder) return sortOrder.sortCloudFiles(list.toMutableList(), foldersBeforeFiles, favoritesFirst) } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/SharedListFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/SharedListFragment.kt index 1c0f8c5063ce..096a5fc3b0a2 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/SharedListFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/SharedListFragment.kt @@ -44,12 +44,12 @@ class SharedListFragment : override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - searchFragment = true + isSearchFragment = true } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - adapter.setShowMetadata(false) + adapter?.setShowMetadata(false) currentSearchType = SearchType.SHARED_FILTER searchEvent = SearchEvent("", SearchRemoteOperation.SearchType.SHARED_FILTER) menuItemAddRemoveValue = MenuItemAddRemove.REMOVE_GRID_AND_SORT @@ -66,7 +66,7 @@ class SharedListFragment : } } - override fun getSearchRemoteOperation(currentUser: User?, event: SearchEvent?): RemoteOperation<*> = + override fun getSearchRemoteOperation(currentUser: User, event: SearchEvent): RemoteOperation<*> = GetSharesRemoteOperation(false, SessionTimeOut(TASK_TIMEOUT, TASK_TIMEOUT)) @Suppress("DEPRECATION") @@ -75,7 +75,7 @@ class SharedListFragment : val fetchResult = ReadFileRemoteOperation(partialFile.remotePath).execute(user, context) if (fetchResult.isSuccess) { val remoteFile = (fetchResult.data[0] as RemoteFile).apply { - val existingFile = containerActivity.storageManager.getFileByDecryptedRemotePath(remotePath) + val existingFile = requireContainerActivity().storageManager.getFileByDecryptedRemotePath(remotePath) // Use previous eTag if exists to prevent break checkForChanges logic in RefreshFolderOperation. // Otherwise RefreshFolderOperation will show empty list @@ -89,7 +89,7 @@ class SharedListFragment : } FileStorageUtils.searchForLocalFileInDefaultPath(file, user.accountName) UnifiedShareSharees.fill(user, listOf(file)) - val savedFile = containerActivity.storageManager.saveFileWithParent(file, context) + val savedFile = requireContainerActivity().storageManager.saveFileWithParent(file, context) savedFile } else { logger.e(SHARED_TAG, "Error fetching file") @@ -113,18 +113,16 @@ class SharedListFragment : } } - private fun fetchAllAndRun(partialFiles: MutableSet?, callback: (MutableSet?) -> Unit) { + private fun fetchAllAndRun(partialFiles: Set, callback: (Set) -> Unit) { lifecycleScope.launch { - if (partialFiles != null) { - val files = partialFiles.toMutableSet().mapNotNull { partialFile -> - fetchFileData(partialFile).also { fetched -> - if (fetched == null) { - SnackbarUtil.show(requireActivity(), R.string.error_retrieving_file) - } + val files = partialFiles.toMutableSet().mapNotNull { partialFile -> + fetchFileData(partialFile).also { fetched -> + if (fetched == null) { + SnackbarUtil.show(requireActivity(), R.string.error_retrieving_file) } } - callback(files.toHashSet()) } + callback(files.toHashSet()) } } @@ -154,7 +152,7 @@ class SharedListFragment : override fun onItemClicked(file: OCFile) { // if in multi select keep mock file - if (adapter.isMultiSelect()) { + if (adapter?.isMultiSelect() == true) { super.onItemClicked(file) } else { fetchFileAndRun(file) { fetched -> @@ -163,7 +161,7 @@ class SharedListFragment : } } - override fun onFileActionChosen(itemId: Int, checkedFiles: MutableSet?): Boolean { + override fun onFileActionChosen(itemId: Int, checkedFiles: Set): Boolean { // fetch all files and run selected action if (itemId != R.id.action_select_all_action_menu && itemId != R.id.action_deselect_all_action_menu) { fetchAllAndRun(checkedFiles) { files -> diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/albums/AlbumItemsFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/albums/AlbumItemsFragment.kt index 1e8a7cfbebe0..5fab841eb5e6 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/albums/AlbumItemsFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/albums/AlbumItemsFragment.kt @@ -515,8 +515,6 @@ class AlbumItemsFragment : //endregion //region Item interaction - override fun getColumnsCount(): Int = columnSize - override fun isLoading(): Boolean = false override fun onHeaderClicked() = Unit diff --git a/app/src/main/java/com/owncloud/android/ui/interfaces/OCFileListFragmentInterface.java b/app/src/main/java/com/owncloud/android/ui/interfaces/OCFileListFragmentInterface.java index 77119296f829..f9264f74fc47 100644 --- a/app/src/main/java/com/owncloud/android/ui/interfaces/OCFileListFragmentInterface.java +++ b/app/src/main/java/com/owncloud/android/ui/interfaces/OCFileListFragmentInterface.java @@ -18,8 +18,6 @@ */ public interface OCFileListFragmentInterface { - int getColumnsCount(); - void onShareIconClick(OCFile file); void showShareDetailView(OCFile file); From 27f706f257a6339663e9abb3716122ad74e629b0 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 26 Oct 2026 19:26:57 +0300 Subject: [PATCH 3/4] simplify Signed-off-by: alperozturk96 --- .../ui/activity/FileDisplayActivity.kt | 8 +- .../ui/activity/FolderPickerActivity.kt | 2 +- .../android/ui/fragment/FolderEncryption.kt | 5 +- .../android/ui/fragment/GalleryFragment.kt | 7 +- .../android/ui/fragment/OCFileListFragment.kt | 648 +++++++----------- .../OCFileListMultiChoiceModeListener.kt | 176 +++++ 6 files changed, 428 insertions(+), 418 deletions(-) create mode 100644 app/src/main/java/com/owncloud/android/ui/fragment/OCFileListMultiChoiceModeListener.kt diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt index a207717dbd49..b2ba6f9dfafc 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt @@ -735,7 +735,7 @@ class FileDisplayActivity : } listOfFilesFragment?.isSearchFragment = true - listOfFilesFragment?.setCurrentSearchType(searchEvent) + listOfFilesFragment?.updateSearchType(searchEvent) } // endregion @@ -1462,7 +1462,7 @@ class FileDisplayActivity : folderRefreshScheduler.start() if (ocFileListFragment?.isSearchFragment == true) { - ocFileListFragment?.setSearchArgs(ocFileListFragment?.arguments) + ocFileListFragment?.loadSearchState(ocFileListFragment?.arguments) } if (isAlbumsFragment || isAlbumItemsFragment) { highlightNavigationViewItem(R.id.nav_album) @@ -2469,7 +2469,7 @@ class FileDisplayActivity : // OCFileSearchTask may still run during rename, so use a single refresh path to avoid // flicker/inconsistent filenames. if (fragment?.isSearchFragment == true) { - fragment.cancelAndRetriggerSearch() + fragment.restartSearch() } else { fragment?.adapter?.updateFile(renamedFile) } @@ -2972,7 +2972,7 @@ class FileDisplayActivity : this.leftFragment = SharedListFragment() } - listOfFilesFragment?.setCurrentSearchType(event) + listOfFilesFragment?.updateSearchType(event) updateActionBarTitleAndHomeButton(null) } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt index 20e5e4a5ed77..9abf7924e82a 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt @@ -534,7 +534,7 @@ open class FolderPickerActivity : return } - listOfFilesFragment?.onMessageEvent( + listOfFilesFragment?.onSearchEvent( SearchEvent( query, SearchRemoteOperation.SearchType.FILE_SEARCH diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FolderEncryption.kt b/app/src/main/java/com/owncloud/android/ui/fragment/FolderEncryption.kt index ac52f7cab1b8..8d8c31403101 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FolderEncryption.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FolderEncryption.kt @@ -45,7 +45,10 @@ class FolderEncryption(private val fragment: OCFileListFragment) { val provider = fragment.arbitraryDataProvider val publicKey = provider.getValue(user, EncryptionUtils.PUBLIC_KEY) val privateKey = provider.getValue(user, EncryptionUtils.PRIVATE_KEY) - val client = fragment.clientFactory.create(user) + val client = fragment.clientRepository.getOwncloudClient() ?: run { + Log_OC.e(TAG, "client is not available, cannot toggle encryption of $remotePath") + return@withContext false + } val result = ToggleEncryptionRemoteOperation(localId, remotePath, shouldBeEncrypted) .execute(client) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/GalleryFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/GalleryFragment.kt index f515dabda3e6..df6cacb57753 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/GalleryFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/GalleryFragment.kt @@ -54,7 +54,6 @@ import com.owncloud.android.ui.activity.ToolbarActivity import com.owncloud.android.ui.adapter.filelist.CommonOCFileListAdapterInterface import com.owncloud.android.ui.adapter.GalleryAdapter import com.owncloud.android.ui.asynctasks.GallerySearchTask -import com.owncloud.android.ui.events.ChangeMenuEvent import com.owncloud.android.ui.fragment.GalleryFragmentBottomSheetDialog.MediaState import com.owncloud.android.ui.fragment.helper.ColumnCount import com.owncloud.android.ui.fragment.helper.GalleryPinchListener @@ -272,12 +271,8 @@ class GalleryFragment : fda?.setMainFabVisible(false) } - override fun onMessageEvent(changeMenuEvent: ChangeMenuEvent) { - super.onMessageEvent(changeMenuEvent) - } - private fun handleSearchEvent() { - prepareCurrentSearch(searchEvent) + applySearchEvent(searchEvent) if (galleryAdapter?.isEmpty() != false) { setEmptyListMessage(EmptyListState.LOADING) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt index 5c0e385c9f31..320cdb5e496d 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt @@ -24,19 +24,15 @@ import android.content.Intent import android.os.Bundle import android.os.Handler import android.os.Looper -import android.util.Log import android.view.ActionMode import android.view.LayoutInflater import android.view.Menu -import android.view.MenuItem import android.view.View import android.view.ViewGroup -import android.widget.AbsListView import androidx.annotation.IdRes import androidx.annotation.OptIn import androidx.annotation.StringRes -import androidx.core.content.ContextCompat -import androidx.drawerlayout.widget.DrawerLayout +import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetBehavior @@ -49,10 +45,8 @@ import com.nextcloud.client.device.DeviceInfo import com.nextcloud.client.di.Injectable import com.nextcloud.client.documentscan.AppScanOptionalFeature import com.nextcloud.client.documentscan.DocumentScanActivity -import com.nextcloud.client.jobs.BackgroundJobManager -import com.nextcloud.client.network.ClientFactory -import com.nextcloud.client.systembars.SystemBarBackgroundCallbacks import com.nextcloud.client.utils.Throttler +import com.nextcloud.repository.ClientRepository import com.nextcloud.ui.fileactions.FileAction import com.nextcloud.ui.fileactions.FileActionHandler import com.nextcloud.ui.fileactions.FileActionHost @@ -90,7 +84,6 @@ import com.owncloud.android.lib.resources.files.ToggleFavoriteRemoteOperation import com.owncloud.android.lib.resources.status.OCCapability import com.owncloud.android.lib.resources.status.Type import com.owncloud.android.ui.CompletionCallback -import com.owncloud.android.ui.activity.AlbumsPickerActivity import com.owncloud.android.ui.activity.DrawerActivity import com.owncloud.android.ui.activity.FileActivity import com.owncloud.android.ui.activity.FileDisplayActivity @@ -119,7 +112,9 @@ import com.owncloud.android.utils.FileSortOrder import com.owncloud.android.utils.FileStorageUtils import com.owncloud.android.utils.PermissionUtil import com.owncloud.android.utils.theme.ThemeUtils -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode @@ -128,10 +123,6 @@ import javax.inject.Inject import kotlin.jvm.optionals.getOrNull import com.owncloud.android.lib.resources.files.SearchRemoteOperation.SearchType as RemoteSearchType -/** - * A Fragment that lists all files and folders in a given path. - * TODO refactor to get rid of direct dependency on FileDisplayActivity - */ @Suppress("TooManyFunctions", "LargeClass") open class OCFileListFragment : ExtendedListFragment(), @@ -139,8 +130,9 @@ open class OCFileListFragment : OCFileListBottomSheetActions, Injectable { + //region Dependencies and state @Inject - lateinit var clientFactory: ClientFactory + lateinit var clientRepository: ClientRepository @Inject lateinit var throttler: Throttler @@ -151,9 +143,6 @@ open class OCFileListFragment : @Inject lateinit var arbitraryDataProvider: ArbitraryDataProvider - @Inject - lateinit var backgroundJobManager: BackgroundJobManager - @Inject lateinit var fastScrollUtils: FastScrollUtils @@ -193,9 +182,6 @@ open class OCFileListFragment : var containerActivity: ContainerActivity? = null private set - /** - * Use this to query the [OCFile] that is currently being displayed by this fragment - */ var currentFile: OCFile? = null protected set @@ -221,15 +207,16 @@ open class OCFileListFragment : private var onlyFoldersClickable = false private var fileSelectable = false private var limitToMimeType: String? = null - private var activeActionMode: ActionMode? = null - private var isActionModeNew = false private var searchTask: OCFileListSearchTask? = null private var fabMain: FloatingActionButton? = null - private val multiChoiceModeListener = MultiChoiceModeListener() + private val multiChoiceModeListener by lazy { OCFileListMultiChoiceModeListener(this) } private val mainHandler = Handler(Looper.getMainLooper()) + private val activeActionMode: ActionMode? + get() = multiChoiceModeListener.activeActionMode + private val activeSearchEvent: SearchEvent? - get() = searchEvent?.takeIf { it.isSet() } + get() = searchEvent?.takeIf { it.hasQueryOrFilter() } private val capabilities: OCCapability get() = requireContainerActivity().storageManager.getCapability(accountManager.user.accountName) @@ -241,9 +228,13 @@ open class OCFileListFragment : ADD_GRID_AND_SORT_WITH_SEARCH } + fun requireContainerActivity(): ContainerActivity = + checkNotNull(containerActivity) { "Fragment $this is not attached to a ContainerActivity" } + //endregion + + //region Lifecycle override fun onAttach(context: Context) { super.onAttach(context) - Log_OC.i(TAG, "onAttach") containerActivity = requireNotNull(context as? ContainerActivity) { "$context must implement ${ContainerActivity::class.java.simpleName}" } @@ -260,30 +251,34 @@ open class OCFileListFragment : fileListLayoutManager = FileListLayoutManager(this, preferences) val state = savedInstanceState ?: arguments - setSearchArgs(state) + loadSearchState(state) currentFile = state.getParcelableArgument(KEY_FILE, OCFile::class.java) isSearchFragment = activeSearchEvent != null folderEncryption = FolderEncryption(this) encryptedClickHandler = EncryptedFolderClickHandler(this) e2eeDialogPresenter = E2EEDialogPresenter(this) + + Log_OC.d( + TAG, + "Created ${javaClass.simpleName}: folder=${currentFile?.remotePath}, " + + "searchType=$currentSearchType, isSearchFragment=$isSearchFragment" + ) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - Log_OC.i(TAG, "onCreateView() start") val view = super.onCreateView(inflater, container, savedInstanceState) val state = savedInstanceState ?: arguments - setSearchArgs(state) + loadSearchState(state) if (state?.getBoolean(ARG_ALLOW_CONTEXTUAL_ACTIONS, false) == true) { - setChoiceModeAsMultipleModal(state) + setupSelectionMode(state) } fabMain = requireActivity().findViewById(R.id.fab_main)?.also { viewThemeUtils.material.themeFAB(it) } - Log_OC.i(TAG, "onCreateView() end") return view } @@ -296,8 +291,7 @@ open class OCFileListFragment : @Suppress("DEPRECATION") override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - Log_OC.i(TAG, "onActivityCreated() start") - prepareOCFileList(savedInstanceState) + setupFileList(savedInstanceState) listDirectory(MainApp.isOnlyOnDevice()) } @@ -314,8 +308,6 @@ open class OCFileListFragment : } override fun onResume() { - // Don't handle search events if we're coming back from back stack - // The fragment has already been properly restored in onCreate/onActivityCreated if (currentFile != null) { super.onResume() return @@ -323,7 +315,7 @@ open class OCFileListFragment : val activity = activity ?: return activity.intent.getParcelableArgument(SEARCH_EVENT, SearchEvent::class.java)?.let { searchEvent = it } - activeSearchEvent?.let { handleSearchEvent(it) } + activeSearchEvent?.let { startSearch(it) } super.onResume() } @@ -342,7 +334,7 @@ open class OCFileListFragment : outState.putParcelable(KEY_CURRENT_SEARCH_TYPE, currentSearchType) activeSearchEvent?.let { outState.putParcelable(SEARCH_EVENT, it) } } - multiChoiceModeListener.storeStateIn(outState) + multiChoiceModeListener.saveState(outState) } override fun onStop() { @@ -361,8 +353,10 @@ open class OCFileListFragment : searchTask?.cancel() super.onDetach() } + //endregion - fun setSearchArgs(state: Bundle?) { + //region Setup + fun loadSearchState(state: Bundle?) { currentSearchType = state?.getParcelableArgument(KEY_CURRENT_SEARCH_TYPE, SearchType::class.java) ?: SearchType.NO_SEARCH state?.getParcelableArgument(SEARCH_EVENT, SearchEvent::class.java)?.let { searchEvent = it } @@ -372,7 +366,7 @@ open class OCFileListFragment : } } - fun prepareOCFileList(savedInstanceState: Bundle?) { + private fun setupFileList(savedInstanceState: Bundle?) { if (savedInstanceState != null) { currentFile = savedInstanceState.getParcelableArgument(KEY_FILE, OCFile::class.java) } @@ -392,12 +386,11 @@ open class OCFileListFragment : registerFabListener() } - // do not touch search event if previously searched if (!isSearchFragment) { searchEvent = arguments.getParcelableArgument(SEARCH_EVENT, SearchEvent::class.java) } - prepareCurrentSearch(searchEvent) - setEmptyView(searchEvent) + applySearchEvent(searchEvent) + showEmptyStateForSearch(searchEvent) mSortButton?.setOnClickListener { SortOrderUi.showDialog(parentFragmentManager, preferences.getSortOrderByFolder(currentFile)) @@ -436,21 +429,23 @@ open class OCFileListFragment : recyclerView?.let { fastScrollUtils.applyFastScroll(it) } } - protected fun setChoiceModeAsMultipleModal(savedInstanceState: Bundle?) { - savedInstanceState?.let { multiChoiceModeListener.loadStateFrom(it) } + private fun setupSelectionMode(savedState: Bundle?) { + savedState?.let { multiChoiceModeListener.restoreState(it) } (requireActivity() as? FileActivity)?.addDrawerListener(multiChoiceModeListener) } + //endregion + //region FAB and bottom navigation fun registerFabListener() { val fileActivity = activity as? FileActivity if (fileActivity == null) { - Log_OC.w(TAG, "activity is null cannot register fab listener") + Log_OC.w(TAG, "Cannot register FAB listener: activity is not a FileActivity") return } val fab = fabMain if (fab == null) { - Log_OC.w(TAG, "mFabMain is null cannot register fab listener") + Log_OC.w(TAG, "Cannot register FAB listener: FAB is not part of this screen") return } @@ -461,7 +456,7 @@ open class OCFileListFragment : private fun showCreateBottomSheet(fileActivity: FileActivity) { val currentDir = currentFile if (currentDir == null) { - Log_OC.w(TAG, "currentDir is null cannot open bottom sheet dialog") + Log_OC.w(TAG, "Cannot open create bottom sheet: current folder is unknown") return } @@ -483,7 +478,6 @@ open class OCFileListFragment : } fun setFabVisible(visible: Boolean) { - // is not available in FolderPickerActivity val fab = fabMain ?: return val fileDisplayActivity = activity as? FileDisplayActivity @@ -515,7 +509,7 @@ open class OCFileListFragment : } } - fun slideHideBottomBehaviourForBottomNavigationView(visible: Boolean) { + private fun setBottomNavigationVisible(visible: Boolean) { val drawerActivity = activity as? DrawerActivity ?: return drawerActivity.bottomNavigationView.slideHideBottomBehavior(visible) } @@ -527,7 +521,9 @@ open class OCFileListFragment : } private fun canCreateInside(folder: OCFile): Boolean = folder.canCreateFileAndFolder() && !isReadOnlyFolder(folder) + //endregion + //region Create and upload actions override fun createFolder(encrypted: Boolean) { if (encrypted) { encryptedClickHandler.onNewEncryptedFolder() @@ -540,7 +536,7 @@ open class OCFileListFragment : fun showCreateFolderDialog(encrypted: Boolean) { val activity = activity if (activity == null) { - Log_OC.e(TAG, "activity is null, cannot create a folder") + Log_OC.e(TAG, "Cannot show create folder dialog: fragment is not attached") return } @@ -564,6 +560,7 @@ open class OCFileListFragment : override fun directCameraUpload() { val fileDisplayActivity = activity as? FileDisplayActivity if (fileDisplayActivity == null) { + Log_OC.w(TAG, "Cannot start camera upload: activity is not a FileDisplayActivity") SnackbarUtil.show(view, getString(R.string.error_starting_direct_camera_upload)) return } @@ -573,10 +570,10 @@ open class OCFileListFragment : return } - showDirectCameraUploadAlertDialog(fileDisplayActivity) + showCameraUploadTypeDialog(fileDisplayActivity) } - private fun showDirectCameraUploadAlertDialog(fileDisplayActivity: FileDisplayActivity) { + private fun showCameraUploadTypeDialog(fileDisplayActivity: FileDisplayActivity) { val builder = MaterialAlertDialogBuilder(fileDisplayActivity) .setTitle(R.string.upload_direct_camera_promt) .setIcon(R.mipmap.ic_launcher) @@ -603,10 +600,10 @@ open class OCFileListFragment : val fileDisplayActivity = activity as? FileDisplayActivity val currentFile = currentFile if (fileDisplayActivity == null || currentFile == null || !currentFile.isFolder) { - Log.w( + Log_OC.w( TAG, - "scanDocUpload: Failed to start doc scanning, fileDisplayActivity=$fileDisplayActivity, " + - "currentFile=$currentFile" + "Cannot start document scan: activity=${activity?.javaClass?.simpleName}, " + + "folder=${currentFile?.remotePath}, isFolder=${currentFile?.isFolder}" ) SnackbarUtil.show(this, R.string.error_starting_doc_scan) return @@ -634,19 +631,19 @@ open class OCFileListFragment : override fun uploadFiles() { val fileActivity = activity as? FileActivity if (fileActivity == null) { - Log_OC.w(TAG, "Activity is null, cant upload files") + Log_OC.w(TAG, "Cannot upload files: activity is not a FileActivity") return } val user = fileActivity.user.getOrNull() if (user == null) { - Log_OC.w(TAG, "User not exist, cant upload files") + Log_OC.w(TAG, "Cannot upload files: no user is logged in") return } - val file = currentFile - if (file == null) { - Log_OC.w(TAG, "File is null cannot determine isWithinEncryptedFolder, cant upload files") + val folder = currentFile + if (folder == null) { + Log_OC.w(TAG, "Cannot upload files: current folder is unknown") return } @@ -654,7 +651,7 @@ open class OCFileListFragment : fileActivity, user, FileDisplayActivity.REQUEST_CODE__SELECT_FILES_FROM_FILE_SYSTEM, - file.isEncrypted + folder.isEncrypted ) } @@ -668,7 +665,10 @@ open class OCFileListFragment : containerActivity?.fileOperationsHelper ?.openRichWorkspaceWithTextEditor(currentFile, url, requireContext()) }, - onError = { SnackbarUtil.show(view, R.string.failed_to_start_editor) } + onError = { + Log_OC.e(TAG, "Cannot create rich workspace for $remotePath") + SnackbarUtil.show(view, R.string.failed_to_start_editor) + } ) } @@ -690,7 +690,9 @@ open class OCFileListFragment : ChooseTemplateDialogFragment.newInstance(currentFile, creator, headline) .show(requireActivity().supportFragmentManager, DIALOG_CREATE_DOCUMENT) } + //endregion + //region List item interaction override fun onShareIconClick(file: OCFile) { containerActivity?.showDetails(file, SHARING_TAB) } @@ -708,27 +710,27 @@ open class OCFileListFragment : } override fun onHeaderClicked() { - val file = currentFile ?: return - if (file.richWorkspace.isNullOrEmpty()) return + val folder = currentFile ?: return + if (folder.richWorkspace.isNullOrEmpty()) return val adapter = adapter if (adapter == null || adapter.isMultiSelect()) return val fileDisplayActivity = containerActivity as? FileDisplayActivity ?: return - fileDisplayActivity.startRichWorkspacePreview(file) + fileDisplayActivity.startRichWorkspacePreview(folder) } override fun onLongItemClicked(file: OCFile): Boolean { val activity = activity ?: return true if (activeActionMode != null) { - toggleItemToCheckedList(file) - } else { - activity.startActionMode(multiChoiceModeListener) - commonAdapter?.addCheckedFile(file) + toggleFileSelection(file) + return true } - updateActionModeFile(file) + activity.startActionMode(multiChoiceModeListener) + commonAdapter?.addCheckedFile(file) + multiChoiceModeListener.onSelectionChanged(file) return true } @@ -739,16 +741,16 @@ open class OCFileListFragment : @OptIn(markerClass = [UnstableApi::class]) override fun onItemClicked(file: OCFile?, sourceView: View?) { if (file == null) { - Log_OC.d(TAG, "Null object in ListAdapter!") + Log_OC.w(TAG, "Ignoring click on a null list item") return } val commonAdapter = commonAdapter when { - commonAdapter?.isMultiSelect() == true -> toggleItemToCheckedList(file) - commonAdapter != null && file.isFolder -> folderOnItemClick(file, commonAdapter.getItemPosition(file)) + commonAdapter?.isMultiSelect() == true -> toggleFileSelection(file) + commonAdapter != null && file.isFolder -> onFolderClicked(file, commonAdapter.getItemPosition(file)) fileSelectable -> finishWithSelectedFile(file) - !onlyFoldersClickable -> fileOnItemClick(file, sourceView) + !onlyFoldersClickable -> onFileClicked(file, sourceView) } } @@ -762,31 +764,31 @@ open class OCFileListFragment : } } - private fun folderOnItemClick(file: OCFile, position: Int) { + private fun onFolderClicked(folder: OCFile, position: Int) { val folderPickerActivity = requireActivity() as? FolderPickerActivity if (folderPickerActivity != null) { - val filenameErrorMessage = FileNameValidator.checkFileName(file.fileName, capabilities, requireContext()) + val filenameErrorMessage = FileNameValidator.checkFileName(folder.fileName, capabilities, requireContext()) if (filenameErrorMessage != null) { SnackbarUtil.show(folderPickerActivity, filenameErrorMessage) return } } - if (file.isEncrypted) { - encryptedClickHandler.onEncryptedFolderClick(file, position) + if (folder.isEncrypted) { + encryptedClickHandler.onEncryptedFolderClick(folder, position) return } isSearchFragment = false setEmptyListMessage(EmptyListState.LOADING) - browseToFolder(file, position) + browseToFolder(folder, position) } - private fun fileOnItemClick(file: OCFile, sourceView: View?) { - val errorMessageId = getOpenRestrictionMessage(file) + private fun onFileClicked(file: OCFile, sourceView: View?) { + val restrictionMessage = getOpenRestrictionMessage(file) val recyclerView = recyclerView - if (recyclerView != null && errorMessageId != null) { - Snackbar.make(recyclerView, errorMessageId, Snackbar.LENGTH_LONG).show() + if (recyclerView != null && restrictionMessage != null) { + Snackbar.make(recyclerView, restrictionMessage, Snackbar.LENGTH_LONG).show() return } @@ -803,7 +805,7 @@ open class OCFileListFragment : } ) - else -> handlePendingDownloadFile(file) + else -> openFile(file) } } @@ -814,15 +816,15 @@ open class OCFileListFragment : else -> null } - private fun handlePendingDownloadFile(file: OCFile) { + private fun openFile(file: OCFile) { if (!isAccountManagerInitialized()) { - Log_OC.e(TAG, "AccountManager not yet initialized") + Log_OC.e(TAG, "Cannot open ${file.remotePath}: account manager is not initialized") return } val context = context if (context == null) { - Log_OC.e(TAG, "context not available") + Log_OC.e(TAG, "Cannot open ${file.remotePath}: fragment is not attached") return } @@ -838,14 +840,11 @@ open class OCFileListFragment : webViewAvailable && hasTextEditor(file) && !file.isEncrypted -> TextEditorWebView.startTextEditor(file, context) - supportsDirectEditing(file, webViewAvailable) -> + canOpenAsRichDocument(file, webViewAvailable) -> containerActivity?.fileOperationsHelper?.openFileAsRichDocument(file, context) fileDisplayActivity != null -> { fileDisplayActivity.startDownloadForPreview(file, currentFile) - - // Checks if the file is small enough to be previewed immediately without showing progress. - // If the file is smaller than or equal to 1MB, it can be displayed directly. if (file.isFileEligibleForImmediatePreview) { fileDisplayActivity.setFileIDForImmediatePreview(file.fileId) } @@ -855,7 +854,7 @@ open class OCFileListFragment : private fun hasTextEditor(file: OCFile): Boolean = editorUtils.getEditor(accountManager.user, file.mimeType) != null - private fun supportsDirectEditing(file: OCFile, webViewAvailable: Boolean): Boolean { + private fun canOpenAsRichDocument(file: OCFile, webViewAvailable: Boolean): Boolean { if (!webViewAvailable) return false val user = accountManager.user @@ -868,19 +867,14 @@ open class OCFileListFragment : capability.richDocumentsDirectEditing.isTrue && !file.isEncrypted } + //endregion - private fun toggleItemToCheckedList(file: OCFile) { + //region Selection + private fun toggleFileSelection(file: OCFile) { commonAdapter?.run { if (isCheckedFile(file)) removeCheckedFile(file) else addCheckedFile(file) } - updateActionModeFile(file) - } - - private fun updateActionModeFile(file: OCFile) { - isActionModeNew = false - val actionMode = activeActionMode ?: return - actionMode.invalidate() - commonAdapter?.notifyItemChanged(file) + multiChoiceModeListener.onSelectionChanged(file) } @SuppressLint("NotifyDataSetChanged") @@ -896,11 +890,9 @@ open class OCFileListFragment : fun exitSelectionMode() { activeActionMode?.finish() } + //endregion - private fun invalidateActionMode() { - activeActionMode?.invalidate() - } - + //region File actions fun openActionsMenu(filesCount: Int, checkedFiles: Set, isOverflow: Boolean) { throttler.run(OVERFLOW_CLICK_THROTTLE_KEY) { val actionsToHide = FileAction.getFileListActionsToHide(checkedFiles) @@ -936,15 +928,9 @@ open class OCFileListFragment : } } - /** - * Start the appropriate action(s) on the currently selected files given menu selected by the user. - * - * @param checkedFiles List of files selected by the user on which the action should be performed - * @return 'true' if the menu selection started any action, 'false' otherwise. - */ open fun onFileActionChosen(@IdRes itemId: Int, checkedFiles: Set): Boolean { if (checkedFiles.isEmpty()) return false - if (onListActionChosen(itemId, checkedFiles)) return true + if (handleListAction(itemId, checkedFiles)) return true val containerActivity = containerActivity ?: return false val host = FileActionHost(containerActivity, parentFragmentManager, view, currentFile, activeActionMode) @@ -956,17 +942,17 @@ open class OCFileListFragment : return isHandled } - private fun onListActionChosen(@IdRes itemId: Int, checkedFiles: Set): Boolean { + private fun handleListAction(@IdRes itemId: Int, checkedFiles: Set): Boolean { when { itemId == R.id.action_see_details && checkedFiles.size == SINGLE_SELECTION -> showDetailsAndHideSortGroup(checkedFiles.first()) itemId == R.id.action_download_file || itemId == R.id.action_sync_file -> { - syncAndCheckFiles(checkedFiles) + syncFilesIfSpaceAvailable(checkedFiles) exitSelectionMode() } - itemId == R.id.action_move_or_copy -> moveOrCopy(checkedFiles) + itemId == R.id.action_move_or_copy -> startMoveOrCopy(checkedFiles) itemId == R.id.action_select_all_action_menu -> selectAllFiles(true) @@ -988,7 +974,7 @@ open class OCFileListFragment : } } - private fun moveOrCopy(checkedFiles: Set) { + private fun startMoveOrCopy(checkedFiles: Set) { val invalidFilename = findInvalidFilename(checkedFiles) if (invalidFilename != null) { SnackbarUtil.show( @@ -999,12 +985,13 @@ open class OCFileListFragment : } if (!FileNameValidator.checkParentRemotePaths(checkedFiles.toList(), capabilities, requireContext())) { + Log_OC.w(TAG, "Cannot move or copy: current path contains an invalid folder name, browsing to root") browseToRoot() SnackbarUtil.show(requireActivity(), R.string.file_name_validator_current_path_is_invalid) return } - pickFolderForMoveOrCopy(checkedFiles) + openFolderPickerForMoveOrCopy(checkedFiles) } private fun findInvalidFilename(files: Set): String? = files.firstNotNullOfOrNull { @@ -1012,20 +999,19 @@ open class OCFileListFragment : } @Suppress("DEPRECATION") - private fun pickFolderForMoveOrCopy(checkedFiles: Set) { + private fun openFolderPickerForMoveOrCopy(checkedFiles: Set) { val activity = requireActivity() val paths = checkedFiles.mapTo(ArrayList(checkedFiles.size)) { it.remotePath } val action = Intent(activity, FolderPickerActivity::class.java).apply { putStringArrayListExtra(FolderPickerActivity.EXTRA_FILE_PATHS, paths) putExtra(FolderPickerActivity.EXTRA_FOLDER, currentFile) - // No animation since we stay in the same folder addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION) putExtra(FolderPickerActivity.EXTRA_ACTION, FolderPickerActivity.MOVE_OR_COPY) } activity.startActivityForResult(action, FileDisplayActivity.REQUEST_CODE__MOVE_OR_COPY_FILES) } - private fun syncAndCheckFiles(files: Collection) { + private fun syncFilesIfSpaceAvailable(files: Collection) { if (files.isEmpty()) return (containerActivity as? FileActivity)?.showSyncLoadingDialog(files.any { it.isFolder }) @@ -1035,12 +1021,13 @@ open class OCFileListFragment : if (FileStorageUtils.checkIfEnoughSpace(file)) { containerActivity?.fileOperationsHelper?.syncFileOrFolder(file, index == lastIndex, false) } else { - showSpaceErrorDialog(file, FileOperationsHelper.getAvailableSpaceOnDevice()) + Log_OC.w(TAG, "Not enough space to sync ${file.remotePath}") + showNotEnoughSpaceDialog(file, FileOperationsHelper.getAvailableSpaceOnDevice()) } } } - private fun showSpaceErrorDialog(file: OCFile, availableSpaceOnDevice: Long) { + private fun showNotEnoughSpaceDialog(file: OCFile, availableSpaceOnDevice: Long) { parentFragmentManager.setFragmentResultListener( SyncFileNotEnoughSpaceDialogFragment.REQUEST_KEY, viewLifecycleOwner @@ -1058,51 +1045,46 @@ open class OCFileListFragment : SyncFileNotEnoughSpaceDialogFragment.newInstance(file, availableSpaceOnDevice) .show(parentFragmentManager, ConfirmationDialogFragment.FTAG_CONFIRMATION) } + //endregion - /** - * Call this, when the user presses the up button. - * - * Tries to move up the current folder one level. If the parent folder was removed from the database, it continues - * browsing up until finding an existing folders. - * - * @return Count of folder levels browsed up. - */ + //region Navigation fun onBrowseUp(): Int { - val file = currentFile - if (file == null || file.isRootDirectory) return 0 + val folder = currentFile + if (folder == null || folder.isRootDirectory) return 0 - val (levelsUp, target) = parentFolderFinder.getParent(file, containerActivity?.storageManager) - if (target == null) { - Log_OC.e(TAG, "onBrowseUp: could not resolve parent, staying put") + val (levelsUp, parent) = parentFolderFinder.getParent(folder, containerActivity?.storageManager) + if (parent == null) { + Log_OC.e(TAG, "Cannot browse up from ${folder.remotePath}: no existing parent folder found") return 0 } - currentFile = target - setFileDepth(target) + Log_OC.d(TAG, "Browsed up $levelsUp level(s) from ${folder.remotePath} to ${parent.remotePath}") + currentFile = parent + setFileDepth(parent) - if (target.isRootDirectory && currentSearchType != SearchType.NO_SEARCH && activeSearchEvent != null) { + if (parent.isRootDirectory && currentSearchType != SearchType.NO_SEARCH && activeSearchEvent != null) { isSearchFragment = true } - updateFileList() + reloadAndRestorePosition() return levelsUp } - private fun updateFileList() { + private fun reloadAndRestorePosition() { listDirectory(currentFile, MainApp.isOnlyOnDevice()) onRefresh(false) restoreIndexAndTopPosition() } - fun browseToFolder(file: OCFile?, position: Int) { - setFileDepth(file) + fun browseToFolder(folder: OCFile?, position: Int) { + setFileDepth(folder) if (currentSearchType == SearchType.FAVORITE_SEARCH) { - resetMenuItems() + resetSortAndGridButtons() } - listDirectory(file, MainApp.isOnlyOnDevice()) - containerActivity?.onBrowsedDownTo(file) + listDirectory(folder, MainApp.isOnlyOnDevice()) + containerActivity?.onBrowsedDownTo(folder) saveIndexAndTopPosition(position) } @@ -1121,10 +1103,9 @@ open class OCFileListFragment : val fileDepth: OCFileDepth? get() = currentFileDepth + //endregion - /** - * Calls [OCFileListFragment.listDirectory] with a null parameter - */ + //region Directory listing fun listDirectory(onlyOnDevice: Boolean) { listDirectory(null, onlyOnDevice) } @@ -1133,16 +1114,10 @@ open class OCFileListFragment : listDirectory(directory, null, onlyOnDevice) } - /** - * Lists the given directory on the view. When the input parameter is null, it will either refresh the last known - * directory. list the root if there never was a directory. - * - * @param directory File to be listed - */ - fun listDirectory(directory: OCFile?, file: OCFile?, onlyOnDevice: Boolean) { + fun listDirectory(directory: OCFile?, highlightedFile: OCFile?, onlyOnDevice: Boolean) { if (isSearchFragment) { activeSearchEvent?.let { - handleSearchEvent(it) + startSearch(it) mRefreshListLayout?.isRefreshing = false } return @@ -1150,38 +1125,38 @@ open class OCFileListFragment : val storageManager = containerActivity?.storageManager if (storageManager == null) { - Log_OC.d(TAG, "fileDataStorageManager is null") + Log_OC.w(TAG, "Cannot list directory: storage manager is not available") return } val directoryToList = resolveDirectoryToList(directory, storageManager) if (directoryToList == null) { - Log_OC.e(TAG, "directory is null, no files, wait for sync") + Log_OC.w(TAG, "Cannot list directory: folder is not in the database yet, waiting for sync") return } val mimeType = limitToMimeType if (mimeType == null) { - Log_OC.w(TAG, "mLimitToMimeType is null") + Log_OC.w(TAG, "Cannot list directory: list is not set up yet") return } val adapter = adapter if (adapter == null) { - Log_OC.e(TAG, "❗oc file list adapter is null, cannot list directory❗") + Log_OC.w(TAG, "Cannot list directory: adapter is not set up yet") return } - Log_OC.i(TAG, "listing directory: " + directoryToList.decryptedRemotePath) + Log_OC.i(TAG, "Listing directory ${directoryToList.decryptedRemotePath}, onlyOnDevice=$onlyOnDevice") adapter.swapDirectory(accountManager.user, directoryToList, storageManager, onlyOnDevice, mimeType) val previousDirectory = currentFile currentFile = directoryToList - updateLayout() - scrollAfterListing(adapter, file, previousDirectory, directoryToList) + updateUiForCurrentFolder() + scrollToHighlightedFileOrTop(adapter, highlightedFile, previousDirectory, directoryToList) } - private fun scrollAfterListing( + private fun scrollToHighlightedFileOrTop( adapter: OCFileListAdapter, highlightedFile: OCFile?, previousDirectory: OCFile?, @@ -1207,15 +1182,15 @@ open class OCFileListFragment : val candidate = directory ?: currentFile ?: storageManager.getFileByPath(OCFile.ROOT_PATH) ?: return null if (candidate.isFolder) return candidate - Log_OC.w(TAG, "You see, that is not a directory -> $candidate") + Log_OC.d(TAG, "${candidate.remotePath} is not a folder, listing its parent instead") return storageManager.getFileById(candidate.parentId) } fun refreshDirectory() { isSearchFragment = false - val currentFile = currentFile ?: return - setFabVisible(canCreateInside(currentFile)) - listDirectory(currentFile, MainApp.isOnlyOnDevice()) + val folder = currentFile ?: return + setFabVisible(canCreateInside(folder)) + listDirectory(folder, MainApp.isOnlyOnDevice()) } val adapterFiles: List @@ -1228,21 +1203,21 @@ open class OCFileListFragment : val isEmpty: Boolean get() = adapter?.isEmpty ?: true - private fun updateLayout() { + private fun updateUiForCurrentFolder() { fileListLayoutManager?.setLayoutViewMode() - updateSortButton() + updateSortButtonLabel() setLayoutSwitchButton() - val file = currentFile - val readOnly = isReadOnlyFolder(file) + val folder = currentFile + val readOnly = isReadOnlyFolder(folder) setFabVisible(!hideFab && !readOnly) - slideHideBottomBehaviourForBottomNavigationView(!hideFab) - setFabEnabled(file != null && !readOnly && (file.canCreateFileAndFolder() || file.isOfflineOperation)) + setBottomNavigationVisible(!hideFab) + setFabEnabled(folder != null && !readOnly && (folder.canCreateFileAndFolder() || folder.isOfflineOperation)) - invalidateActionMode() + activeActionMode?.invalidate() } - private fun updateSortButton() { + private fun updateSortButtonLabel() { val sortButton = mSortButton ?: return val sortOrder = if (currentSearchType == SearchType.FAVORITE_SEARCH) { preferences.getSortOrderByType(FileSortOrder.Type.favoritesListView, FileSortOrder.SORT_A_TO_Z) @@ -1251,7 +1226,9 @@ open class OCFileListFragment : } sortButton.setText(SortOrderUi.labelRes(sortOrder)) } + //endregion + //region Menu @Deprecated("Deprecated in Java") override fun onPrepareOptionsMenu(menu: Menu) { if (menuItemAddRemoveValue == MenuItemAddRemove.REMOVE_GRID_AND_SORT) { @@ -1259,13 +1236,13 @@ open class OCFileListFragment : } if (currentSearchType == SearchType.FAVORITE_SEARCH) { - resetMenuItems() + resetSortAndGridButtons() } else { - updateSortAndGridMenuItems() + updateSortAndGridButtons() } } - private fun updateSortAndGridMenuItems() { + private fun updateSortAndGridButtons() { val switchGridViewButton = mSwitchGridViewButton ?: return val sortButton = mSortButton ?: return @@ -1282,23 +1259,15 @@ open class OCFileListFragment : switchGridViewButton.visibility = View.GONE } - MenuItemAddRemove.DO_NOTHING -> Log_OC.v(TAG, "Kept the options menu default structure") + MenuItemAddRemove.DO_NOTHING -> Unit } } - private fun resetMenuItems() { + private fun resetSortAndGridButtons() { menuItemAddRemoveValue = MenuItemAddRemove.ADD_GRID_AND_SORT_WITH_SEARCH - updateSortAndGridMenuItems() + updateSortAndGridButtons() } - /** - * Returns the navigation drawer menu item corresponding to this fragment. - * - * OCFileListFragment is the parent for GalleryFragment, SharedListFragment, - * and GroupfolderListFragment. It also internally handles listing favorites, - * shared files, or recently modified items via search events. This method - * checks the current fragment type and search state to give correct drawer menu ID. - */ val menuItemId: Int get() = when { javaClass == GalleryFragment::class.java -> R.id.nav_gallery @@ -1316,20 +1285,23 @@ open class OCFileListFragment : else -> R.id.nav_all_files } + //endregion - protected fun prepareCurrentSearch(event: SearchEvent?) { - if (event == null || !event.isSet()) return - setCurrentSearchType(event) - prepareActionBarItems(event) + //region Search + protected fun applySearchEvent(event: SearchEvent?) { + if (event == null || !event.hasQueryOrFilter()) return + updateSearchType(event) + updateMenuForSearch(event) } - fun setCurrentSearchType(event: SearchEvent) { + fun updateSearchType(event: SearchEvent) { event.toSearchType()?.let { currentSearchType = it } } - protected fun prepareActionBarItems(event: SearchEvent?) { - val searchType = event?.searchType - if (searchType == RemoteSearchType.FAVORITE_SEARCH || searchType == RemoteSearchType.RECENTLY_MODIFIED_SEARCH) { + private fun updateMenuForSearch(event: SearchEvent) { + if (event.searchType == RemoteSearchType.FAVORITE_SEARCH || + event.searchType == RemoteSearchType.RECENTLY_MODIFIED_SEARCH + ) { menuItemAddRemoveValue = MenuItemAddRemove.REMOVE_SORT } @@ -1338,7 +1310,7 @@ open class OCFileListFragment : } } - protected fun setEmptyView(event: SearchEvent?) { + private fun showEmptyStateForSearch(event: SearchEvent?) { val emptyListType = when (event?.searchType) { RemoteSearchType.FILE_SEARCH -> SearchType.FILE_SEARCH RemoteSearchType.FAVORITE_SEARCH -> SearchType.FAVORITE_SEARCH @@ -1355,20 +1327,16 @@ open class OCFileListFragment : currentSearchType = SearchType.NO_SEARCH } - protected fun handleSearchEvent(event: SearchEvent) { + private fun startSearch(event: SearchEvent) { if (event.searchType == RemoteSearchType.PHOTO_SEARCH) return if (searchTask?.isFinished() == false) { - searchEvent?.let { - Log_OC.d( - TAG, - "OCFileListSearchTask already running skipping new api call for search event: " + - it.searchType - ) - } + Log_OC.d(TAG, "Skipping ${event.searchType} search, previous search is still running") return } + Log_OC.i(TAG, "Starting ${event.searchType} search") + if (searchTask == null) { activity?.runOnUiThread { adapter?.removeAllFiles() @@ -1376,22 +1344,22 @@ open class OCFileListFragment : } } - prepareCurrentSearch(event) + applySearchEvent(event) isSearchFragment = true setFabVisible(false) mainHandler.post { - updateSortButton() + updateSortButtonLabel() fileListLayoutManager?.setLayoutViewMode() } - startSearchTask(event) + launchSearchTask(event) } - private fun startSearchTask(event: SearchEvent) { + private fun launchSearchTask(event: SearchEvent) { val currentUser = accountManager.user val remoteOperation = if (currentSearchType == SearchType.RECENT_FILES_SEARCH) { - createRecentFilesSearchRemoteOperation() + createRecentFilesSearchOperation() } else { getSearchRemoteOperation(currentUser, event) } @@ -1417,7 +1385,7 @@ open class OCFileListFragment : return SearchRemoteOperation(event.searchQuery, event.searchType, searchOnlyFolders, capability) } - private fun createRecentFilesSearchRemoteOperation(): RemoteOperation<*> { + private fun createRecentFilesSearchOperation(): RemoteOperation<*> { val capability = capabilities val nowSeconds = System.currentTimeMillis() / AppConstants.MILLIS_PER_SECOND @@ -1431,7 +1399,7 @@ open class OCFileListFragment : override fun onRefresh() { val event = activeSearchEvent if (isSearchFragment && event != null) { - handleSearchEvent(event) + startSearch(event) mRefreshListLayout?.isRefreshing = false return } @@ -1440,10 +1408,10 @@ open class OCFileListFragment : super.onRefresh() } - fun cancelAndRetriggerSearch() { + fun restartSearch() { searchTask?.cancel() searchTask = null - searchEvent?.let { handleSearchEvent(it) } + searchEvent?.let { startSearch(it) } } val isSearchEventFavorite: Boolean @@ -1455,15 +1423,18 @@ open class OCFileListFragment : fun shouldNavigateBackToAllFiles(): Boolean = this is GalleryFragment || isSearchEventFavorite || isSearchEventShared - private fun SearchEvent.isSet(): Boolean = searchQuery.isNotEmpty() || searchType in SEARCH_TYPES_WITHOUT_QUERY + private fun SearchEvent.hasQueryOrFilter(): Boolean = + searchQuery.isNotEmpty() || searchType in SEARCH_TYPES_WITHOUT_QUERY + //endregion + //region Event bus @Subscribe(threadMode = ThreadMode.MAIN) - open fun onMessageEvent(changeMenuEvent: ChangeMenuEvent) { - Log_OC.d(TAG, "event bus --- change menu event triggered") + fun onChangeMenuEvent(event: ChangeMenuEvent) { + Log_OC.d(TAG, "Leaving search, resetting search and menu state") arguments?.clear() resetSearchAttributes() - resetMenuItems() + resetSortAndGridButtons() (activity as? FileDisplayActivity)?.run { invalidateOptionsMenu() @@ -1473,217 +1444,83 @@ open class OCFileListFragment : } currentFile?.let { setFabVisible(canCreateInside(it)) } - slideHideBottomBehaviourForBottomNavigationView(true) + setBottomNavigationVisible(true) } @Subscribe(threadMode = ThreadMode.BACKGROUND) - fun onMessageEvent(event: CommentsEvent) { + fun onCommentsEvent(event: CommentsEvent) { adapter?.markCommentsAsRead(event.remoteId) } - @Subscribe(threadMode = ThreadMode.BACKGROUND) - fun onMessageEvent(event: FavoriteEvent) { - try { - val client = clientFactory.create(accountManager.user) - val result = ToggleFavoriteRemoteOperation(event.shouldFavorite, event.remotePath).execute(client) - if (!result.isSuccess) return - - val removeFromList = currentSearchType == SearchType.FAVORITE_SEARCH && !event.shouldFavorite - setEmptyListMessage(SearchType.FAVORITE_SEARCH) - if (this is GalleryFragment) { - markAsFavorite(event.remotePath, event.shouldFavorite) - } else { - adapter?.updateFavoriteState(event.remotePath, event.shouldFavorite, removeFromList) - } - } catch (e: ClientFactory.CreationException) { - Log_OC.e(TAG, "Error processing event", e) - } - } - - @Subscribe(threadMode = ThreadMode.BACKGROUND) - fun onMessageEvent(event: SearchEvent) { - handleSearchEvent(event) - } - @Subscribe(threadMode = ThreadMode.MAIN) - fun onMessageEvent(event: EncryptionEvent) { - event.onResult(this) - } - - @Subscribe(threadMode = ThreadMode.BACKGROUND) - fun onMessageEvent(event: FileLockEvent) { - val user = accountManager.user - - try { - val client = clientFactory.createNextcloudClient(user) - val result = ToggleFileLockRemoteOperation(event.shouldLock, event.filePath).execute(client) - if (result.isSuccess) { - // TODO only refresh the modified file? - mainHandler.post { onRefresh() } - return - } - } catch (e: ClientFactory.CreationException) { - Log_OC.e(TAG, "Cannot create client", e) - } - - showFileLockError() - } - - private fun showFileLockError() { - val recyclerView = recyclerView ?: return - Snackbar.make(recyclerView, R.string.error_file_lock, Snackbar.LENGTH_LONG).show() - } - - fun requireContainerActivity(): ContainerActivity = - checkNotNull(containerActivity) { "Fragment $this is not attached to a ContainerActivity" } - - /** - * Handler for multiple selection mode. - * - * Manages input from the user when one or more files or folders are selected in the list. - * - * Also listens to changes in navigation drawer to hide and recover multiple selection when it's opened and closed. - */ - private inner class MultiChoiceModeListener : - AbsListView.MultiChoiceModeListener, - DrawerLayout.DrawerListener { - - private var actionModeClosedByDrawer = false - private val selectionWhenActionModeClosedByDrawer = HashSet() - - override fun onDrawerSlide(drawerView: View, slideOffset: Float) = Unit - - override fun onDrawerOpened(drawerView: View) = Unit - - /** - * When the navigation drawer is closed, action mode is recovered in the same state as was when the drawer was - * (started to be) opened. - */ - override fun onDrawerClosed(drawerView: View) { - if (!actionModeClosedByDrawer || selectionWhenActionModeClosedByDrawer.isEmpty()) return - - activity?.startActionMode(this) - adapter?.setCheckedItem(selectionWhenActionModeClosedByDrawer) - activeActionMode?.invalidate() - selectionWhenActionModeClosedByDrawer.clear() - } - - /** - * If the action mode is active when the navigation drawer starts to move, the action mode is closed and the - * selection stored to be recovered when the drawer is closed. - */ - override fun onDrawerStateChanged(newState: Int) { - val actionMode = activeActionMode - if (newState != DrawerLayout.STATE_DRAGGING || actionMode == null) return - - (recyclerView?.adapter as? OCFileListAdapter)?.let { - selectionWhenActionModeClosedByDrawer.addAll(it.getCheckedItems()) + fun onFavoriteEvent(event: FavoriteEvent) { + lifecycleScope.launch { + val client = clientRepository.getOwncloudClient() + if (client == null) { + Log_OC.e(TAG, "Cannot toggle favorite for ${event.remotePath}: client is not available") + return@launch } - actionMode.finish() - actionModeClosedByDrawer = true - } - - override fun onItemCheckedStateChanged(mode: ActionMode?, position: Int, id: Long, checked: Boolean) = Unit - - @SuppressFBWarnings("RV") - override fun onCreateActionMode(mode: ActionMode?, menu: Menu): Boolean { - activeActionMode = mode - isActionModeNew = true - - // fake menu to be able to use bottom sheet instead - requireActivity().menuInflater.inflate(R.menu.custom_menu_placeholder, menu) - val placeholderItem = menu.findItem(R.id.custom_menu_placeholder_item) - placeholderItem.icon?.let { - placeholderItem.icon = viewThemeUtils.platform.colorDrawable( - it, - ContextCompat.getColor(requireContext(), R.color.white) - ) + val result = withContext(Dispatchers.IO) { + ToggleFavoriteRemoteOperation(event.shouldFavorite, event.remotePath).execute(client) } - - mode?.invalidate() - colorStatusBarForActionMode() - setFabVisible(false) - - if (this@OCFileListFragment is GalleryFragment && activity is AlbumsPickerActivity) { - placeholderItem.isVisible = false - menu.findItem(R.id.add_to_album).isVisible = true + if (!result.isSuccess) { + Log_OC.e(TAG, "Cannot toggle favorite for ${event.remotePath}: ${result.code}") + return@launch } - commonAdapter?.setMultiSelect(true) - return true - } - - private fun colorStatusBarForActionMode() { - val activity = requireActivity() - val statusBarColor = ContextCompat.getColor(requireContext(), R.color.action_mode_background) - viewThemeUtils.platform.colorStatusBar(activity, statusBarColor) - SystemBarBackgroundCallbacks.colorStatusBar(activity, statusBarColor) + showFavoriteChange(event) } + } - override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean { - val checkedCount = commonAdapter?.getCheckedItems()?.size ?: 0 - - activeActionMode?.title = resources.getQuantityString( - R.plurals.items_selected_count, - checkedCount, - checkedCount - ) - - if (checkedCount == 0 && !isActionModeNew) { - exitSelectionMode() - } - - isMultipleFileSelectedForCopyOrMove = checkedCount > 0 - return true + private fun showFavoriteChange(event: FavoriteEvent) { + val removeFromList = currentSearchType == SearchType.FAVORITE_SEARCH && !event.shouldFavorite + setEmptyListMessage(SearchType.FAVORITE_SEARCH) + if (this is GalleryFragment) { + markAsFavorite(event.remotePath, event.shouldFavorite) + } else { + adapter?.updateFavoriteState(event.remotePath, event.shouldFavorite, removeFromList) } + } - override fun onActionItemClicked(mode: ActionMode?, item: MenuItem): Boolean { - val commonAdapter = commonAdapter ?: return true - val checkedFiles = commonAdapter.getCheckedItems() - - when (item.itemId) { - R.id.custom_menu_placeholder_item -> - openActionsMenu(commonAdapter.getFilesCount(), checkedFiles, false) - - R.id.add_to_album -> - (this@OCFileListFragment as? GalleryFragment)?.addImagesToAlbum(checkedFiles) - } - - return true - } + @Subscribe(threadMode = ThreadMode.BACKGROUND) + fun onSearchEvent(event: SearchEvent) { + startSearch(event) + } - override fun onDestroyActionMode(mode: ActionMode?) { - activeActionMode = null + @Subscribe(threadMode = ThreadMode.MAIN) + fun onEncryptionEvent(event: EncryptionEvent) { + event.onResult(this) + } - if (!hideFab && !isSearchFragment) { - currentFile?.let { setFabVisible(it.canCreateFileAndFolder()) } + @Subscribe(threadMode = ThreadMode.MAIN) + fun onFileLockEvent(event: FileLockEvent) { + lifecycleScope.launch { + val client = clientRepository.getNextcloudClient() + if (client == null) { + Log_OC.e(TAG, "Cannot toggle lock for ${event.filePath}: client is not available") + showFileLockError() + return@launch } - activity?.let { - viewThemeUtils.platform.resetStatusBar(it) - SystemBarBackgroundCallbacks.apply(it, viewThemeUtils) + val result = withContext(Dispatchers.IO) { + ToggleFileLockRemoteOperation(event.shouldLock, event.filePath).execute(client) } - - commonAdapter?.run { - setMultiSelect(false) - clearCheckedItems() + if (!result.isSuccess) { + Log_OC.e(TAG, "Cannot toggle lock for ${event.filePath}: ${result.code}") + showFileLockError() + return@launch } - isMultipleFileSelectedForCopyOrMove = false - } - - fun storeStateIn(outState: Bundle) { - outState.putBoolean(KEY_ACTION_MODE_CLOSED_BY_DRAWER, actionModeClosedByDrawer) + onRefresh() } + } - fun loadStateFrom(savedInstanceState: Bundle) { - actionModeClosedByDrawer = savedInstanceState.getBoolean( - KEY_ACTION_MODE_CLOSED_BY_DRAWER, - actionModeClosedByDrawer - ) - } + private fun showFileLockError() { + val recyclerView = recyclerView ?: return + Snackbar.make(recyclerView, R.string.error_file_lock, Snackbar.LENGTH_LONG).show() } + //endregion companion object { val TAG: String = OCFileListFragment::class.java.simpleName @@ -1706,7 +1543,6 @@ open class OCFileListFragment : const val SEARCH_EVENT = "SEARCH_EVENT" const val KEY_CURRENT_SEARCH_TYPE = "CURRENT_SEARCH_TYPE" private val KEY_FILE = "$MY_PACKAGE.extra.FILE" - private const val KEY_ACTION_MODE_CLOSED_BY_DRAWER = "KILLED_ACTION_MODE" private const val DIALOG_CREATE_FOLDER = "DIALOG_CREATE_FOLDER" private const val DIALOG_CREATE_DOCUMENT = "DIALOG_CREATE_DOCUMENT" diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListMultiChoiceModeListener.kt b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListMultiChoiceModeListener.kt new file mode 100644 index 000000000000..4d3892693885 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListMultiChoiceModeListener.kt @@ -0,0 +1,176 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.fragment + +import android.app.Activity +import android.os.Bundle +import android.view.ActionMode +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.widget.AbsListView +import androidx.core.content.ContextCompat +import androidx.drawerlayout.widget.DrawerLayout +import com.nextcloud.client.systembars.SystemBarBackgroundCallbacks +import com.owncloud.android.R +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.ui.activity.AlbumsPickerActivity +import com.owncloud.android.ui.adapter.filelist.OCFileListAdapter +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings + +internal class OCFileListMultiChoiceModeListener(private val fragment: OCFileListFragment) : + AbsListView.MultiChoiceModeListener, + DrawerLayout.DrawerListener { + + var activeActionMode: ActionMode? = null + private set + + private var isSelectionUntouched = false + private var closedByDrawer = false + private val selectionClosedByDrawer = HashSet() + + fun onSelectionChanged(file: OCFile) { + isSelectionUntouched = false + val actionMode = activeActionMode ?: return + actionMode.invalidate() + fragment.commonAdapter?.notifyItemChanged(file) + } + + fun saveState(outState: Bundle) { + outState.putBoolean(KEY_CLOSED_BY_DRAWER, closedByDrawer) + } + + fun restoreState(savedState: Bundle) { + closedByDrawer = savedState.getBoolean(KEY_CLOSED_BY_DRAWER, closedByDrawer) + } + + //region Drawer + override fun onDrawerSlide(drawerView: View, slideOffset: Float) = Unit + + override fun onDrawerOpened(drawerView: View) = Unit + + override fun onDrawerClosed(drawerView: View) { + if (!closedByDrawer || selectionClosedByDrawer.isEmpty()) return + + Log_OC.d(TAG, "Drawer closed, restoring selection of ${selectionClosedByDrawer.size} file(s)") + fragment.activity?.startActionMode(this) + fragment.adapter?.setCheckedItem(selectionClosedByDrawer) + activeActionMode?.invalidate() + selectionClosedByDrawer.clear() + } + + override fun onDrawerStateChanged(newState: Int) { + val actionMode = activeActionMode + if (newState != DrawerLayout.STATE_DRAGGING || actionMode == null) return + + (fragment.recyclerView?.adapter as? OCFileListAdapter)?.let { + selectionClosedByDrawer.addAll(it.getCheckedItems()) + } + + actionMode.finish() + closedByDrawer = true + } + //endregion + + //region Action mode + override fun onItemCheckedStateChanged(mode: ActionMode?, position: Int, id: Long, checked: Boolean) = Unit + + @SuppressFBWarnings("RV") + override fun onCreateActionMode(mode: ActionMode?, menu: Menu): Boolean { + Log_OC.d(TAG, "Selection mode started") + activeActionMode = mode + isSelectionUntouched = true + + val activity = fragment.requireActivity() + activity.menuInflater.inflate(R.menu.custom_menu_placeholder, menu) + val placeholderItem = menu.findItem(R.id.custom_menu_placeholder_item) + tintPlaceholderIcon(placeholderItem) + + mode?.invalidate() + colorStatusBar(activity) + fragment.setFabVisible(false) + + if (fragment is GalleryFragment && activity is AlbumsPickerActivity) { + placeholderItem.isVisible = false + menu.findItem(R.id.add_to_album).isVisible = true + } + + fragment.commonAdapter?.setMultiSelect(true) + return true + } + + override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean { + val checkedCount = fragment.commonAdapter?.getCheckedItems()?.size ?: 0 + + activeActionMode?.title = fragment.resources.getQuantityString( + R.plurals.items_selected_count, + checkedCount, + checkedCount + ) + + if (checkedCount == 0 && !isSelectionUntouched) { + fragment.exitSelectionMode() + } + + OCFileListFragment.isMultipleFileSelectedForCopyOrMove = checkedCount > 0 + return true + } + + override fun onActionItemClicked(mode: ActionMode?, item: MenuItem): Boolean { + val commonAdapter = fragment.commonAdapter ?: return true + val checkedFiles = commonAdapter.getCheckedItems() + + when (item.itemId) { + R.id.custom_menu_placeholder_item -> + fragment.openActionsMenu(commonAdapter.getFilesCount(), checkedFiles, false) + + R.id.add_to_album -> (fragment as? GalleryFragment)?.addImagesToAlbum(checkedFiles) + } + + return true + } + + override fun onDestroyActionMode(mode: ActionMode?) { + Log_OC.d(TAG, "Selection mode finished") + activeActionMode = null + + if (!fragment.hideFab && !fragment.isSearchFragment) { + fragment.currentFile?.let { fragment.setFabVisible(it.canCreateFileAndFolder()) } + } + + fragment.activity?.let { + fragment.viewThemeUtils.platform.resetStatusBar(it) + SystemBarBackgroundCallbacks.apply(it, fragment.viewThemeUtils) + } + + fragment.commonAdapter?.run { + setMultiSelect(false) + clearCheckedItems() + } + + OCFileListFragment.isMultipleFileSelectedForCopyOrMove = false + } + //endregion + + private fun tintPlaceholderIcon(placeholderItem: MenuItem) { + val icon = placeholderItem.icon ?: return + val white = ContextCompat.getColor(fragment.requireContext(), R.color.white) + placeholderItem.icon = fragment.viewThemeUtils.platform.colorDrawable(icon, white) + } + + private fun colorStatusBar(activity: Activity) { + val statusBarColor = ContextCompat.getColor(fragment.requireContext(), R.color.action_mode_background) + fragment.viewThemeUtils.platform.colorStatusBar(activity, statusBarColor) + SystemBarBackgroundCallbacks.colorStatusBar(activity, statusBarColor) + } + + companion object { + private const val TAG = "OCFileListMultiChoiceModeListener" + private const val KEY_CLOSED_BY_DRAWER = "KILLED_ACTION_MODE" + } +} From 1d9bbe9c55aa187a5595e52323cdca0d6f572563 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 26 Oct 2026 19:47:21 +0300 Subject: [PATCH 4/4] simplify Signed-off-by: alperozturk96 --- .../android/ui/fragment/OCFileListMultiChoiceModeListener.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListMultiChoiceModeListener.kt b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListMultiChoiceModeListener.kt index 4d3892693885..df00d14c251b 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListMultiChoiceModeListener.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListMultiChoiceModeListener.kt @@ -1,7 +1,7 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-License-Identifier: AGPL-3.0-or-later */ package com.owncloud.android.ui.fragment