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..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 @@ -674,7 +674,7 @@ class FileDisplayActivity : ON_DEVICE == action -> { refreshOrInitOCFileListFragment() - listOfFilesFragment?.setCurrentSearchType(SearchType.ON_DEVICE) + listOfFilesFragment?.currentSearchType = SearchType.ON_DEVICE updateActionBarTitleAndHomeButton(null) } } @@ -735,7 +735,7 @@ class FileDisplayActivity : } listOfFilesFragment?.isSearchFragment = true - listOfFilesFragment?.setCurrentSearchType(searchEvent) + listOfFilesFragment?.updateSearchType(searchEvent) } // endregion @@ -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) } } }) } @@ -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) } @@ -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) { @@ -2970,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/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..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 @@ -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 @@ -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) @@ -101,13 +104,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..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 @@ -75,7 +74,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 +97,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 +163,7 @@ class GalleryFragment : binding?.paginationLoader?.removeCallbacks(showPaginationLoader) - adapter = null + galleryAdapter = null super.onDestroyView() } @@ -214,18 +213,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 +236,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 +247,7 @@ class GalleryFragment : override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) columnsCount = defaultColumnsCount(newConfig.isLandscape()) - adapter?.changeColumn(columnsCount) + galleryAdapter?.changeColumn(columnsCount) showAllGalleryItems() } @@ -258,7 +257,8 @@ class GalleryFragment : handleSearchEvent() } - override fun getCommonAdapter(): CommonOCFileListAdapterInterface? = adapter + override val commonAdapter: CommonOCFileListAdapterInterface? + get() = galleryAdapter val currentMediaState: MediaState? get() = bottomSheet?.currMediaState @@ -271,14 +271,10 @@ class GalleryFragment : fda?.setMainFabVisible(false) } - override fun onMessageEvent(changeMenuEvent: ChangeMenuEvent) { - super.onMessageEvent(changeMenuEvent) - } - private fun handleSearchEvent() { - prepareCurrentSearch(searchEvent) + applySearchEvent(searchEvent) - if (adapter?.isEmpty() != false) { + if (galleryAdapter?.isEmpty() != false) { setEmptyListMessage(EmptyListState.LOADING) } @@ -305,7 +301,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 +322,7 @@ class GalleryFragment : endDate = result.lastTimestamp } - if (adapter?.isEmpty() == true) { + if (galleryAdapter?.isEmpty() == true) { setEmptyListMessage(SearchType.GALLERY_SEARCH) } @@ -369,6 +365,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 +376,7 @@ class GalleryFragment : photoSearchTask = GallerySearchTask( this, accountManager.user, - containerActivity.getStorageManager(), + containerActivity.storageManager, endDate, limit ).execute() @@ -405,7 +402,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 +438,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 +449,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 +461,7 @@ class GalleryFragment : if (galleryItems.isEmpty()) { setEmptyListMessage(SearchType.GALLERY_SEARCH) } - adapter?.updateList(galleryItems) + galleryAdapter?.updateList(galleryItems) updateSubtitle(mediaState) scrollToAnchorFile() @@ -550,7 +547,7 @@ class GalleryFragment : } columnsCount = updated - adapter?.changeColumn(columnsCount) + galleryAdapter?.changeColumn(columnsCount) scrollAnchorFile = scrollAnchorFile ?: firstVisibleFile() showAllGalleryItems() } @@ -563,14 +560,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 +576,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.java b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java deleted file mode 100644 index 6e42ac67cd7c..000000000000 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java +++ /dev/null @@ -1,2150 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2026 Alper Ozturk - * SPDX-FileCopyrightText: 2026 Philipp Hasper - * SPDX-FileCopyrightText: 2023-2026 TSI-mc - * SPDX-FileCopyrightText: 2018-2023 Tobias Kaminsky - * SPDX-FileCopyrightText: 2022 Álvaro Brey - * SPDX-FileCopyrightText: 2020 Joris Bodin - * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-FileCopyrightText: 2016-2018 Andy Scherzinger - * SPDX-FileCopyrightText: 2015 ownCloud Inc. - * SPDX-FileCopyrightText: 2015 David A. Velasco - * 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; - -/** - * 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 - 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 - } - - protected MenuItemAddRemove menuItemAddRemoveValue = MenuItemAddRemove.ADD_GRID_AND_SORT_WITH_SEARCH; - - private final List mOriginalMenuItems = new ArrayList<>(); - - private static OCFileDepth fileDepth = OCFileDepth.Root; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setHasOptionsMenu(true); - mMultiChoiceModeListener = new MultiChoiceModeListener(); - fileListLayoutManager = new FileListLayoutManager(this, preferences); - - 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); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - OCFileListFragmentExtensionsKt.listenEncryptionDialogResult(this); - } - - @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; - } - - final var activity = getActivity(); - if (activity == null) { - return; - } - - final Intent intent = activity.getIntent(); - if (IntentExtensionsKt.getParcelableArgument(intent, SEARCH_EVENT, SearchEvent.class) != null) { - searchEvent = IntentExtensionsKt.getParcelableArgument(intent, SEARCH_EVENT, SearchEvent.class); - } - - if (isSearchEventSet(searchEvent)) { - handleSearchEvent(searchEvent); - } - - super.onResume(); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - if (getAdapter() != null) { - getAdapter().cleanup(); - } - } - - /** - * {@inheritDoc} - */ - @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); - - } catch (ClassCastException e) { - throw new IllegalArgumentException(context + " must implement " + - OnEnforceableRefreshListener.class.getSimpleName(), e); - } - } - - public FileFragment.ContainerActivity getContainerActivity() { - return mContainerActivity; - } - - public FolderEncryption getFolderEncryption() { - return folderEncryption; - } - - public EncryptedFolderClickHandler getEncryptedClickHandler() { - return clickHandler; - } - - public E2EEDialogPresenter getE2eeDialogPresenter() { - return e2eeDialogPresenter; - } - - public E2EEActionResolver getE2eeActionResolver() { - return e2eeActionResolver; - } - - public void setSearchArgs(Bundle state) { - SearchType argSearchType = NO_SEARCH; - SearchEvent argSearchEvent = null; - - if (state != null) { - argSearchType = BundleExtensionsKt.getParcelableArgument(state, KEY_CURRENT_SEARCH_TYPE, SearchType.class); - argSearchEvent = BundleExtensionsKt.getParcelableArgument(state, SEARCH_EVENT, SearchEvent.class); - } - - currentSearchType = Objects.requireNonNullElse(argSearchType, NO_SEARCH); - - if (argSearchEvent != null) { - searchEvent = argSearchEvent; - } - - if (searchEvent != null && currentSearchType != NO_SEARCH) { - searchFragment = true; - } - } - - /** - * {@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); - - final Bundle state = savedInstanceState != null ? savedInstanceState : getArguments(); - setSearchArgs(state); - - boolean allowContextualActions = (state != null && state.getBoolean(ARG_ALLOW_CONTEXTUAL_ACTIONS, false)); - if (allowContextualActions) { - setChoiceModeAsMultipleModal(state); - } - - mFabMain = requireActivity().findViewById(R.id.fab_main); - - if (mFabMain != null) { - // is not available in FolderPickerActivity - viewThemeUtils.material.themeFAB(mFabMain); - } - - Log_OC.i(TAG, "onCreateView() end"); - return v; - } - - @Override - public void onDetach() { - setOnRefreshListener(null); - mContainerActivity = null; - - if (searchTask != null) { - searchTask.cancel(); - } - super.onDetach(); - } - - @Override - public void onPause() { - super.onPause(); - if (mAdapter != null) { - mAdapter.cancelAllPendingTasks(); - } - - final var activity = getActivity(); - if (activity != null) { - activity.getIntent().removeExtra(OCFileListFragment.SEARCH_EVENT); - } - } - - /** - * {@inheritDoc} - */ - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - Log_OC.i(TAG, "onActivityCreated() start"); - prepareOCFileList(savedInstanceState); - listDirectory(MainApp.isOnlyOnDevice()); - } - - public void prepareOCFileList(Bundle savedInstanceState) { - if (savedInstanceState != null) { - mFile = BundleExtensionsKt.getParcelableArgument(savedInstanceState, KEY_FILE, OCFile.class); - } - - 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, "") : ""; - - setAdapter(args); - - mHideFab = args != null && args.getBoolean(ARG_HIDE_FAB, false); - - if (mHideFab) { - setFabVisible(false); - } else { - if (mFile != null) { - setFabVisible(canCreateInside(mFile)); - } else { - setFabVisible(true); - } - - registerFabListener(); - } - - if (!searchFragment) { - // do not touch search event if previously searched - if (getArguments() == null) { - searchEvent = null; - } else { - searchEvent = BundleExtensionsKt.getParcelableArgument(getArguments(), SEARCH_EVENT, SearchEvent.class); - } - } - prepareCurrentSearch(searchEvent); - setEmptyView(searchEvent); - - if (mSortButton != null) { - mSortButton.setOnClickListener(v -> SortOrderUi.showDialog(requireFragmentManager(), - preferences.getSortOrderByFolder(mFile))); - } - - if (mSwitchGridViewButton != null) { - mSwitchGridViewButton.setOnClickListener(v -> { - if (fileListLayoutManager != null) { - if (isGridEnabled()) { - fileListLayoutManager.setListAsPreferred(); - } else { - fileListLayoutManager.setGridAsPreferred(); - } - } - setLayoutSwitchButton(); - }); - } - - if (getActivity() instanceof FileDisplayActivity fda) { - fda.updateActionBarTitleAndHomeButton(fda.getCurrentDir()); - } - } - - private boolean isReadOnlyFolder(OCFile folder) { - if (folder == null || mContainerActivity == null) { - return false; - } - - final var storageManager = mContainerActivity.getStorageManager(); - return storageManager != null && storageManager.isReadOnly(folder); - } - - private boolean canCreateInside(OCFile folder) { - return folder.canCreateFileAndFolder() && !isReadOnlyFolder(folder); - } - - 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(); - } - - mAdapter = new OCFileListAdapter( - getActivity(), - accountManager.getUser(), - preferences, - syncedFolderProvider, - mContainerActivity, - this, - hideItemOptions, - isGridViewPreferred, - viewThemeUtils, - thumbnailGenerator, - avatarGenerator - ); - - setRecyclerViewAdapter(mAdapter); - - if (getRecyclerView() != null) { - fastScrollUtils.applyFastScroll(getRecyclerView()); - } - } - - protected void prepareCurrentSearch(SearchEvent event) { - if (isSearchEventSet(event)) { - setCurrentSearchType(event); - prepareActionBarItems(event); - } - } - - /** - * register listener on FAB. - */ - public void registerFabListener() { - if (!(getActivity() instanceof FileActivity fileActivity)) { - Log_OC.w(TAG, "activity is null cannot register fab listener"); - return; - } - - if (mFabMain == null) { - Log_OC.w(TAG, "mFabMain is null cannot register fab listener"); - return; - } - - // 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; - } - - 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) { - if (encrypted) { - clickHandler.onNewEncryptedFolder(); - return; - } - - showCreateFolderDialog(false); - } - - public void showCreateFolderDialog(boolean encrypted) { - final var activity = getActivity(); - if (activity == null) { - Log_OC.e(TAG, "activity is null, cannot create a folder"); - return; - } - - CreateFolderDialogFragment.newInstance(mFile, encrypted) - .show(activity.getSupportFragmentManager(), 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); - - 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(); - - if (fileDisplayActivity == null) { - SnackbarUtil.show(getView(), getString(R.string.error_starting_direct_camera_upload)); - return; - } - - if (!PermissionUtil.checkSelfPermission(fileDisplayActivity, Manifest.permission.CAMERA)) { - PermissionUtil.requestCameraPermission(fileDisplayActivity, PermissionUtil.PERMISSIONS_CAMERA); - return; - } - - showDirectCameraUploadAlertDialog(fileDisplayActivity); - } - - private void showDirectCameraUploadAlertDialog(FileDisplayActivity fileDisplayActivity) { - final MaterialAlertDialogBuilder builder = new 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); - - builder.create(); - 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 - public void scanDocUploadFromApp() { - requireActivity().startActivityForResult( - scanIntentExternalApp, - FileDisplayActivity.REQUEST_CODE__SELECT_CONTENT_FROM_APPS_AUTO_RENAME); - } - - @Override - public boolean isScanDocUploadFromAppAvailable() { - var context = getActivity(); - if (context == null) { - return false; - } - 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; - } - - final var user = fileActivity.getUser(); - if (user.isEmpty()) { - Log_OC.w(TAG,"User not exist, cant upload files"); - return; - } - - final var file = getCurrentFile(); - if (file == null) { - 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); - } - - @Override - public void createRichWorkspace() { - if (!(getActivity() instanceof FileActivity fileActivity)) { - 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; - }); - } - - @Override - public void onShareIconClick(OCFile file) { - mContainerActivity.showDetails(file, 1); - } - - @Override - public void showShareDetailView(OCFile file) { - mContainerActivity.showDetails(file, 1); - } - - @Override - public void showActivityDetailView(OCFile file) { - mContainerActivity.showDetails(file, 0); - } - - @Override - public void onOverflowIconClicked(OCFile file, View view) { - final Set checkedFiles = new HashSet<>(); - checkedFiles.add(file); - openActionsMenu(1, checkedFiles, true); - } - - public void openActionsMenu(final int filesCount, final Set checkedFiles, final boolean isOverflow) { - throttler.run("overflowClick", () -> { - final var actionsToHide = FileAction.Companion.getFileListActionsToHide(checkedFiles); - - final var mimeType = MimeTypeExtensionsKt.resolveMimeType(checkedFiles.iterator().next()); - List endpoints = getCapabilities().getClientIntegrationEndpoints(Type.CONTEXT_MENU, mimeType); - - final var childFragmentManager = getChildFragmentManager(); - final var actionBottomSheet = FileActionsBottomSheet.newInstance(filesCount, checkedFiles, isOverflow, actionsToHide, endpoints) - .setResultListener(childFragmentManager, this, (id) -> onFileActionChosen(id, checkedFiles)); - - if (FragmentExtensionsKt.isDialogFragmentReady(this)) { - actionBottomSheet.show(childFragmentManager, "actions"); - } - }); - } - - public void openCurrentFolderActionsMenu() { - final OCFile currentFolder = getCurrentFile(); - if (currentFolder == null || currentFolder.isRootDirectory()) { - return; - } - - final Set checkedFiles = new HashSet<>(); - checkedFiles.add(currentFolder); - - 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)); - - if (FragmentExtensionsKt.isDialogFragmentReady(this)) { - actionBottomSheet.show(childFragmentManager, "actions"); - } - }); - } - - @Override - public void newDocument() { - ChooseRichDocumentsTemplateDialogFragment.newInstance(mFile, - ChooseRichDocumentsTemplateDialogFragment.Type.DOCUMENT) - .show(requireActivity().getSupportFragmentManager(), DIALOG_CREATE_DOCUMENT); - } - - @Override - public void newSpreadsheet() { - ChooseRichDocumentsTemplateDialogFragment.newInstance(mFile, - ChooseRichDocumentsTemplateDialogFragment.Type.SPREADSHEET) - .show(requireActivity().getSupportFragmentManager(), DIALOG_CREATE_DOCUMENT); - } - - @Override - public void newPresentation() { - ChooseRichDocumentsTemplateDialogFragment.newInstance(mFile, - ChooseRichDocumentsTemplateDialogFragment.Type.PRESENTATION) - .show(requireActivity().getSupportFragmentManager(), DIALOG_CREATE_DOCUMENT); - } - - @Override - public void onHeaderClicked() { - final OCFile file = getCurrentFile(); - if (file == null) { - return; - } - - if (TextUtils.isEmpty(file.getRichWorkspace())) { - return; - } - - final var adapter = getAdapter(); - if (adapter == null || adapter.isMultiSelect()) { - return; - } - - if (!(mContainerActivity instanceof FileDisplayActivity fda)) { - return; - } - - fda.startRichWorkspacePreview(file); - } - - @Override - public void showTemplate(Creator creator, String headline) { - ChooseTemplateDialogFragment.newInstance(mFile, creator, headline).show(requireActivity() - .getSupportFragmentManager(), - DIALOG_CREATE_DOCUMENT); - } - - /** - * 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 static final String KEY_ACTION_MODE_CLOSED_BY_DRAWER = "KILLED_ACTION_MODE"; - - /** - * True when action mode is finished because the drawer was opened - */ - private boolean mActionModeClosedByDrawer; - - /** - * Selected items in list when action mode is closed by drawer - */ - private final Set mSelectionWhenActionModeClosedByDrawer = new HashSet<>(); - - @Override - public void onDrawerSlide(@NonNull View drawerView, float slideOffset) { - // nothing to do - } - - @Override - public void onDrawerOpened(@NonNull View drawerView) { - // nothing to do - } - - /** - * 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; - } - - FragmentActivity actionBarActivity = getActivity(); - if (actionBarActivity != null) { - actionBarActivity.startActionMode(mMultiChoiceModeListener); - } - - getAdapter().setCheckedItem(mSelectionWhenActionModeClosedByDrawer); - - if (mActiveActionMode != null) { - mActiveActionMode.invalidate(); - } - - mSelectionWhenActionModeClosedByDrawer.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. - * - * @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; - } - - if (getRecyclerView() != null && getRecyclerView().getAdapter() instanceof OCFileListAdapter fileListAdapter) { - mSelectionWhenActionModeClosedByDrawer.addAll(fileListAdapter.getCheckedItems()); - } - - mActiveActionMode.finish(); - mActionModeClosedByDrawer = true; - } - - /** - * 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 - } - - /** - * 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; - - // fake menu to be able to use bottom sheet instead - MenuInflater inflater = requireActivity().getMenuInflater(); - inflater.inflate(R.menu.custom_menu_placeholder, menu); - - 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))); - } - - mActiveActionMode.invalidate(); - - //set actionMode color - int statusBarColor = ContextCompat.getColor(requireContext(), R.color.action_mode_background); - viewThemeUtils.platform.colorStatusBar(requireActivity(), statusBarColor); - SystemBarBackgroundCallbacks.colorStatusBar(requireActivity(), statusBarColor); - - // hide FAB in multi selection mode - setFabVisible(false); - - 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); - } - - final var adapter = getCommonAdapter(); - if (adapter != null) { - adapter.setMultiSelect(true); - } - - return true; - } - - - /** - * 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); - } - - // Determine if we need to finish the action mode because there are no items selected - if (checkedCount == 0 && !mIsActionModeNew) { - exitSelectionMode(); - } - - isMultipleFileSelectedForCopyOrMove = (checkedCount > 0); - - return true; - } - - /** - * 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; - } - - /** - * 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()); - } - } - - final var activity = getActivity(); - if (activity != null) { - viewThemeUtils.platform.resetStatusBar(activity); - SystemBarBackgroundCallbacks.apply(activity, viewThemeUtils); - } - - final var adapter = getCommonAdapter(); - if (adapter != null) { - adapter.setMultiSelect(false); - adapter.clearCheckedItems(); - } - - isMultipleFileSelectedForCopyOrMove = false; - } - - public void storeStateIn(Bundle outState) { - outState.putBoolean(KEY_ACTION_MODE_CLOSED_BY_DRAWER, mActionModeClosedByDrawer); - } - - public void loadStateFrom(Bundle savedInstanceState) { - mActionModeClosedByDrawer = savedInstanceState.getBoolean(KEY_ACTION_MODE_CLOSED_BY_DRAWER, - mActionModeClosedByDrawer); - } - } - - /** - * Init listener that will handle interactions in multiple selection mode. - */ - protected void setChoiceModeAsMultipleModal(Bundle savedInstanceState) { - if (savedInstanceState != null) { - mMultiChoiceModeListener.loadStateFrom(savedInstanceState); - } - ((FileActivity) requireActivity()).addDrawerListener(mMultiChoiceModeListener); - } - - /** - * 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); - } - } - mMultiChoiceModeListener.storeStateIn(outState); - } - - @Override - public void onPrepareOptionsMenu(@NonNull Menu menu) { - if (mOriginalMenuItems.isEmpty()) { - mOriginalMenuItems.add(menu.findItem(R.id.action_search)); - } - - if (menuItemAddRemoveValue == MenuItemAddRemove.REMOVE_GRID_AND_SORT) { - menu.removeItem(R.id.action_search); - } - - if (currentSearchType == FAVORITE_SEARCH) { - resetMenuItems(); - } else { - updateSortAndGridMenuItems(); - } - } - - private void updateSortAndGridMenuItems() { - if (mSwitchGridViewButton == null || mSortButton == null) { - return; - } - - switch (menuItemAddRemoveValue) { - case ADD_GRID_AND_SORT_WITH_SEARCH: - mSwitchGridViewButton.setVisibility(View.VISIBLE); - mSortButton.setVisibility(View.VISIBLE); - break; - - case REMOVE_SORT: - mSortButton.setVisibility(View.GONE); - break; - - case REMOVE_GRID_AND_SORT: - mSortButton.setVisibility(View.GONE); - mSwitchGridViewButton.setVisibility(View.GONE); - break; - - case DO_NOTHING: - default: - Log_OC.v(TAG, "Kept the options menu default structure"); - break; - } - } - - /** - * 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. - */ - public int onBrowseUp() { - if (mFile == null || mFile.isRootDirectory()) { - return 0; - } - - final var result = parentFolderFinder.getParent(mFile, mContainerActivity.getStorageManager()); - OCFile target = result.getSecond(); - - if (target == null) { - Log_OC.e(TAG, "onBrowseUp: could not resolve parent, staying put"); - return 0; - } - - mFile = target; - setFileDepth(mFile); - - if (mFile.isRootDirectory() && currentSearchType != NO_SEARCH && isSearchEventSet(searchEvent)) { - searchFragment = true; - } - - updateFileList(); - return result.getFirst(); - } - - private void updateFileList() { - listDirectory(mFile, 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); - } - updateActionModeFile(file); - } - - /** - * 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); - } - } - - @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); - } - - return true; - } - - 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; - } - } - - if (file.isEncrypted()) { - clickHandler.onEncryptedFolderClick(file, position); - } else { - // update state and view of this fragment - searchFragment = false; - setEmptyListMessage(EmptyListState.LOADING); - browseToFolder(file, position); - } - } - - 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; - } - } - - 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; - } - - 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); - } - } - - private boolean supportsDirectEditing(OCFile file, boolean webViewAvailable) { - if (!webViewAvailable) { - return false; - } - - final var account = accountManager.getUser(); - if (account.isAnonymous()) { - return false; - } - - final var capability = - mContainerActivity.getStorageManager().getCapability(account.getAccountName()); - - final var mimeTypes = capability.getRichDocumentsMimeTypeList(); - return mimeTypes != null - && mimeTypes.contains(file.getMimeType()) - && capability.getRichDocumentsDirectEditing().isTrue() - && !file.isEncrypted(); - } - - private void handlePendingDownloadFile(OCFile file) { - if (!isAccountManagerInitialized()) { - Log_OC.e(TAG, "AccountManager not yet initialized"); - return; - } - - 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()); - } - } - } - - @Override - public void onItemClicked(OCFile file) { - onItemClicked(file, null); - } - - @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; - } - - 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); - } - } - } - - public void setFileDepth(OCFile file) { - fileDepth = OCFileExtensionsKt.getDepth(file); - } - - public void resetFileDepth() { - fileDepth = OCFileDepth.Root; - } - - public OCFileDepth getFileDepth() { - return fileDepth; - } - - protected void browseToFolder(OCFile file, int position) { - setFileDepth(file); - - if (currentSearchType == FAVORITE_SEARCH) { - resetMenuItems(); - } - - 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); - } - - /** - * 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; - } - - if (onListActionChosen(itemId, checkedFiles)) { - return true; - } - - 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(); - } - - 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; - } - - if (!FileNameValidator.INSTANCE.checkParentRemotePaths(new ArrayList<>(checkedFiles), getCapabilities(), requireContext())) { - browseToRoot(); - SnackbarUtil.show(requireActivity(), R.string.file_name_validator_current_path_is_invalid); - return true; - } - - 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; - } - - 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; - } - } - - 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()); - } - 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); - } - - - /** - * Use this to query the {@link OCFile} that is currently being displayed by this fragment - * - * @return The currently viewed OCFile - */ - @Nullable - public OCFile getCurrentFile() { - return mFile; - } - - /** - * Calls {@link OCFileListFragment#listDirectory(OCFile, boolean)} with a null parameter - */ - public void listDirectory(boolean onlyOnDevice) { - listDirectory(null, onlyOnDevice); - } - - public void refreshDirectory() { - searchFragment = false; - - if (mFile != null) { - setFabVisible(canCreateInside(mFile)); - } - - final var currentFile = getCurrentFile(); - if (currentFile != null) { - listDirectory(currentFile, MainApp.isOnlyOnDevice()); - } - } - - public void listDirectory(@Nullable OCFile directory, boolean onlyOnDevice) { - listDirectory(directory, null, onlyOnDevice); - } - - private OCFile getDirectoryForListDirectory(@Nullable OCFile directory, FileDataStorageManager storageManager) { - if (directory == null) { - if (mFile != null) { - directory = mFile; - } else { - directory = storageManager.getFileByPath(ROOT_PATH); - } - } - - // 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()); - } - - 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); - } - } - } - - public List getAdapterFiles() { - return mAdapter.getFiles(); - } - - public void updateOCFile(@NonNull OCFile file) { - mAdapter.replaceFile(file); - } - - private void updateLayout() { - if (fileListLayoutManager != null) { - fileListLayoutManager.setLayoutViewMode(); - } - 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); - } - - mSortButton.setText(SortOrderUi.labelRes(sortOrder)); - } - } - - private void invalidateActionMode() { - if (mActiveActionMode != null) { - mActiveActionMode.invalidate(); - } - } - - public CommonOCFileListAdapterInterface getCommonAdapter() { - return mAdapter; - } - - public OCFileListAdapter getAdapter() { - return mAdapter; - } - - public void setCurrentSearchType(SearchEvent event) { - final var searchType = event.toSearchType(); - if (searchType != null) { - currentSearchType = searchType; - } - } - - public void setCurrentSearchType(SearchType searchType) { - currentSearchType = searchType; - } - - public SearchType getCurrentSearchType() { - return currentSearchType; - } - - protected void prepareActionBarItems(SearchEvent event) { - if (event != null) { - switch (event.getSearchType()) { - case FAVORITE_SEARCH: - case RECENTLY_MODIFIED_SEARCH: - menuItemAddRemoveValue = MenuItemAddRemove.REMOVE_SORT; - break; - - default: - // do nothing - break; - } - } - - if (FILE_SEARCH != currentSearchType) { - final var activity = getActivity(); - if (activity != null) { - activity.invalidateOptionsMenu(); - } - } - } - - protected void setEmptyView(SearchEvent event) { - if (event != null) { - switch (event.getSearchType()) { - case FILE_SEARCH: - setEmptyListMessage(FILE_SEARCH); - break; - - case FAVORITE_SEARCH: - setEmptyListMessage(FAVORITE_SEARCH); - break; - - case RECENTLY_MODIFIED_SEARCH: - setEmptyListMessage(RECENT_FILES_SEARCH); - break; - - case SHARED_FILTER: - setEmptyListMessage(SHARED_FILTER); - break; - - default: - setEmptyListMessage(NO_SEARCH); - break; - } - } else { - setEmptyListMessage(NO_SEARCH); - } - } - - @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(); - - if (getActivity() instanceof FileDisplayActivity fda) { - fda.invalidateOptionsMenu(); - fda.getIntent().removeExtra(OCFileListFragment.SEARCH_EVENT); - fda.setupHomeSearchToolbarWithSortAndListButtons(); - fda.updateActionBarTitleAndHomeButton(null); - } - - if (mFile != null) { - setFabVisible(canCreateInside(mFile)); - } - - slideHideBottomBehaviourForBottomNavigationView(true); - } - - private void resetMenuItems() { - menuItemAddRemoveValue = MenuItemAddRemove.ADD_GRID_AND_SORT_WITH_SEARCH; - updateSortAndGridMenuItems(); - } - - public void resetSearchAttributes() { - searchFragment = false; - searchEvent = null; - currentSearchType = NO_SEARCH; - } - - @Subscribe(threadMode = ThreadMode.BACKGROUND) - public void onMessageEvent(CommentsEvent event) { - mAdapter.markCommentsAsRead(event.getRemoteId()); - } - - @Subscribe(threadMode = ThreadMode.BACKGROUND) - public void onMessageEvent(FavoriteEvent event) { - 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); - } - } - - } 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); - } - } - - @Subscribe(threadMode = ThreadMode.BACKGROUND) - public void onMessageEvent(final SearchEvent event) { - 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); - }); - } - - prepareCurrentSearch(event); - searchFragment = true; - setFabVisible(false); - - new Handler(Looper.getMainLooper()).post(() -> { - updateSortButton(); - if (fileListLayoutManager != null) { - fileListLayoutManager.setLayoutViewMode(); - } - }); - - 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()); - } - - searchTask = new OCFileListSearchTask(this, - remoteOperation, - currentUser, event, - SharedListFragment.TASK_TIMEOUT, - preferences, - storageManager); - searchTask.execute(); - } - - 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 RemoteOperation getRecentFilesSearchRemoteOperation() { - String accountName = accountManager.getUser().getAccountName(); - OCCapability capability = mContainerActivity.getStorageManager().getCapability(accountName); - String searchQuery = ""; - - SearchRemoteOperation remoteOperation = new SearchRemoteOperation( - searchQuery, - SearchRemoteOperation.SearchType.RECENTLY_MODIFIED_SEARCH, - false, - capability - ); - - long nowSeconds = System.currentTimeMillis() / AppConstants.MILLIS_PER_SECOND; - long last14DaysTimestamp = nowSeconds - TimeUnit.DAYS.toSeconds(14); - - remoteOperation.setStartDate(last14DaysTimestamp); - remoteOperation.setEndDate(nowSeconds); - remoteOperation.setLimit(100); - - return remoteOperation; - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onMessageEvent(EncryptionEvent event) { - event.onResult(this); - } - - @Subscribe(threadMode = ThreadMode.BACKGROUND) - public void onMessageEvent(FileLockEvent event) { - final User user = accountManager.getUser(); - - try { - NextcloudClient client = clientFactory.createNextcloudClient(user); - ToggleFileLockRemoteOperation operation = new ToggleFileLockRemoteOperation(event.getShouldLock(), event.getFilePath()); - RemoteOperationResult result = operation.execute(client); - - 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(); - } - - } 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(); - } - } - - @Override - public void onStart() { - super.onStart(); - EventBus.getDefault().register(this); - } - - @Override - public void onStop() { - EventBus.getDefault().unregister(this); - super.onStop(); - } - - - @Override - public void onRefresh() { - if (searchFragment && isSearchEventSet(searchEvent)) { - handleSearchEvent(searchEvent); - - if (mRefreshListLayout != null) { - mRefreshListLayout.setRefreshing(false); - } - } else { - searchFragment = false; - super.onRefresh(); - } - } - - public void setSearchFragment(boolean searchFragment) { - this.searchFragment = searchFragment; - } - - public boolean isSearchFragment() { - return searchFragment; - } - - public void cancelAndRetriggerSearch() { - if (searchTask != null) { - searchTask.cancel(); - searchTask = null; - } - if (searchEvent != null) { - handleSearchEvent(searchEvent); - } - } - - /** - * 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; - } - - final var adapter = recyclerView.getAdapter(); - if (adapter instanceof CommonOCFileListAdapterInterface commonInterface) { - commonInterface.selectAll(select); - adapter.notifyDataSetChanged(); - mActiveActionMode.invalidate(); - } - } - - /** - * Exits the multi file selection mode. - */ - public void exitSelectionMode() { - if (mActiveActionMode != null) { - mActiveActionMode.finish(); - } - } - - private boolean isSearchEventSet(SearchEvent event) { - if (event == null) { - return false; - } - 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; - - boolean hasFolder = files.stream().anyMatch(OCFile::isFolder); - - if (mContainerActivity instanceof FileActivity activity) { - activity.showSyncLoadingDialog(hasFolder); - } - - List fileList = new ArrayList<>(files); - for (int i = 0; i < fileList.size(); i++) { - OCFile file = fileList.get(i); - boolean isLast = i == fileList.size() - 1; - - if (FileStorageUtils.checkIfEnoughSpace(file)) { - mContainerActivity.getFileOperationsHelper().syncFileOrFolder(file, isLast, false); - } else { - showSpaceErrorDialog(file, FileOperationsHelper.getAvailableSpaceOnDevice()); - } - } - } - - 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); - } - - @Override - public boolean isLoading() { - return false; - } - - /** - * 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; - } - - // to hide the fab if user is on Albums Fragment - if (getActivity() instanceof FileDisplayActivity fda - && (fda.isAlbumsFragment() - || fda.isAlbumItemsFragment())) { - mFabMain.hide(); - return; - } - - final var activity = getActivity(); - if (activity == null) { - return; - } - - activity.runOnUiThread(() -> { - if (visible) { - mFabMain.show(); - viewThemeUtils.material.themeFAB(mFabMain); - } else { - mFabMain.hide(); - } - - ViewExtensionsKt.slideHideBottomBehavior(mFabMain, visible); - }); - } - - public void slideHideBottomBehaviourForBottomNavigationView(boolean visible) { - if (getActivity() instanceof DrawerActivity drawerActivity) { - ViewExtensionsKt.slideHideBottomBehavior(drawerActivity.getBottomNavigationView(), visible); - } - } - - /** - * 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; - } - - final var activity = getActivity(); - if (activity == null) { - return; - } - - activity.runOnUiThread(() -> { - if (enabled) { - fabMain.setEnabled(true); - viewThemeUtils.material.themeFAB(fabMain); - } else { - fabMain.setEnabled(false); - viewThemeUtils.material.themeFAB(fabMain); - } - }); - } - - /** - * 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; - } - } - - public boolean isEmpty() { - return mAdapter == null || mAdapter.isEmpty(); - } - - public SearchEvent getSearchEvent() { - return searchEvent; - } - - public boolean isSearchEventFavorite() { - return isSearchEvent(SearchRemoteOperation.SearchType.FAVORITE_SEARCH); - } - - public boolean isSearchEventShared() { - return isSearchEvent(SearchRemoteOperation.SearchType.SHARED_FILTER); - } - - private boolean isSearchEvent(SearchRemoteOperation.SearchType givenEvent) { - if (searchEvent == null) { - return false; - } - return searchEvent.getSearchType() == givenEvent; - } - - public boolean shouldNavigateBackToAllFiles() { - return this instanceof GalleryFragment || - isSearchEventFavorite() || - isSearchEventShared(); - } - - public FileListLayoutManager getFileListLayoutManager() { - return fileListLayoutManager; - } -} 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 new file mode 100644 index 000000000000..320cdb5e496d --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt @@ -0,0 +1,1582 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-FileCopyrightText: 2026 Philipp Hasper + * SPDX-FileCopyrightText: 2023-2026 TSI-mc + * SPDX-FileCopyrightText: 2018-2023 Tobias Kaminsky + * SPDX-FileCopyrightText: 2022 Álvaro Brey + * SPDX-FileCopyrightText: 2020 Joris Bodin + * SPDX-FileCopyrightText: 2019 Chris Narkiewicz + * SPDX-FileCopyrightText: 2016-2018 Andy Scherzinger + * SPDX-FileCopyrightText: 2015 ownCloud Inc. + * SPDX-FileCopyrightText: 2015 David A. Velasco + * 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.view.ActionMode +import android.view.LayoutInflater +import android.view.Menu +import android.view.View +import android.view.ViewGroup +import androidx.annotation.IdRes +import androidx.annotation.OptIn +import androidx.annotation.StringRes +import androidx.lifecycle.lifecycleScope +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.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 +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.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 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 +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 + +@Suppress("TooManyFunctions", "LargeClass") +open class OCFileListFragment : + ExtendedListFragment(), + OCFileListFragmentInterface, + OCFileListBottomSheetActions, + Injectable { + + //region Dependencies and state + @Inject + lateinit var clientRepository: ClientRepository + + @Inject + lateinit var throttler: Throttler + + @Inject + lateinit var themeUtils: ThemeUtils + + @Inject + lateinit var arbitraryDataProvider: ArbitraryDataProvider + + @Inject + lateinit var fastScrollUtils: FastScrollUtils + + @Inject + lateinit var editorUtils: EditorUtils + + @Inject + lateinit var syncedFolderProvider: SyncedFolderProvider + + @Inject + lateinit var fileActionHandler: FileActionHandler + + @Inject + lateinit var appScanOptionalFeature: AppScanOptionalFeature + + @Inject + lateinit var thumbnailGenerator: ThumbnailGenerator + + @Inject + lateinit var avatarGenerator: AvatarGenerator + + @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 + + var currentFile: OCFile? = null + protected set + + var adapter: OCFileListAdapter? = null + protected set + + open val commonAdapter: CommonOCFileListAdapterInterface? + get() = adapter + + var currentSearchType: SearchType = SearchType.NO_SEARCH + var isSearchFragment = false + var searchEvent: SearchEvent? = null + protected set + + var hideFab = true + val parentFolderFinder = ParentFolderFinder() + + var fileListLayoutManager: FileListLayoutManager? = null + private set + + protected var menuItemAddRemoveValue = MenuItemAddRemove.ADD_GRID_AND_SORT_WITH_SEARCH + + private var onlyFoldersClickable = false + private var fileSelectable = false + private var limitToMimeType: String? = null + private var searchTask: OCFileListSearchTask? = null + private var fabMain: FloatingActionButton? = null + 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.hasQueryOrFilter() } + + private val capabilities: OCCapability + get() = requireContainerActivity().storageManager.getCapability(accountManager.user.accountName) + + protected enum class MenuItemAddRemove { + DO_NOTHING, + REMOVE_SORT, + REMOVE_GRID_AND_SORT, + 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) + 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) + } + + @Suppress("DEPRECATION") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + fileListLayoutManager = FileListLayoutManager(this, preferences) + + val state = savedInstanceState ?: arguments + 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? { + val view = super.onCreateView(inflater, container, savedInstanceState) + + val state = savedInstanceState ?: arguments + loadSearchState(state) + + if (state?.getBoolean(ARG_ALLOW_CONTEXTUAL_ACTIONS, false) == true) { + setupSelectionMode(state) + } + + fabMain = requireActivity().findViewById(R.id.fab_main)?.also { + viewThemeUtils.material.themeFAB(it) + } + + return view + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + listenEncryptionDialogResult() + } + + @Deprecated("Deprecated in Java") + @Suppress("DEPRECATION") + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + setupFileList(savedInstanceState) + listDirectory(MainApp.isOnlyOnDevice()) + } + + 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) + } + + override fun onResume() { + if (currentFile != null) { + super.onResume() + return + } + + val activity = activity ?: return + activity.intent.getParcelableArgument(SEARCH_EVENT, SearchEvent::class.java)?.let { searchEvent = it } + activeSearchEvent?.let { startSearch(it) } + + super.onResume() + } + + override fun onPause() { + super.onPause() + adapter?.cancelAllPendingTasks() + activity?.intent?.removeExtra(SEARCH_EVENT) + } + + 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.saveState(outState) + } + + override fun onStop() { + EventBus.getDefault().unregister(this) + super.onStop() + } + + override fun onDestroyView() { + super.onDestroyView() + adapter?.cleanup() + } + + override fun onDetach() { + setOnRefreshListener(null) + containerActivity = null + searchTask?.cancel() + super.onDetach() + } + //endregion + + //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 } + + if (searchEvent != null && currentSearchType != SearchType.NO_SEARCH) { + isSearchFragment = true + } + } + + private fun setupFileList(savedInstanceState: Bundle?) { + if (savedInstanceState != null) { + currentFile = savedInstanceState.getParcelableArgument(KEY_FILE, OCFile::class.java) + } + + 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, "") ?: "" + + setAdapter(args) + + hideFab = args?.getBoolean(ARG_HIDE_FAB, false) == true + if (hideFab) { + setFabVisible(false) + } else { + setFabVisible(currentFile?.let { canCreateInside(it) } ?: true) + registerFabListener() + } + + if (!isSearchFragment) { + searchEvent = arguments.getParcelableArgument(SEARCH_EVENT, SearchEvent::class.java) + } + applySearchEvent(searchEvent) + showEmptyStateForSearch(searchEvent) + + mSortButton?.setOnClickListener { + SortOrderUi.showDialog(parentFragmentManager, preferences.getSortOrderByFolder(currentFile)) + } + mSwitchGridViewButton?.setOnClickListener { toggleGridAndListLayout() } + + (activity as? FileDisplayActivity)?.let { it.updateActionBarTitleAndHomeButton(it.currentDir) } + } + + private fun toggleGridAndListLayout() { + fileListLayoutManager?.let { + if (isGridEnabled) it.setListAsPreferred() else it.setGridAsPreferred() + } + setLayoutSwitchButton() + } + + protected open fun setAdapter(args: Bundle?) { + val hideItemOptions = args?.getBoolean(ARG_HIDE_ITEM_OPTIONS, false) == true + val isGridViewPreferred = fileListLayoutManager?.isGridViewPreferred() == true + + adapter = OCFileListAdapter( + requireActivity(), + accountManager.user, + preferences, + syncedFolderProvider, + requireContainerActivity(), + this, + hideItemOptions, + isGridViewPreferred, + viewThemeUtils, + thumbnailGenerator, + avatarGenerator + ) + + setRecyclerViewAdapter(adapter) + recyclerView?.let { fastScrollUtils.applyFastScroll(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, "Cannot register FAB listener: activity is not a FileActivity") + return + } + + val fab = fabMain + if (fab == null) { + Log_OC.w(TAG, "Cannot register FAB listener: FAB is not part of this screen") + return + } + + viewThemeUtils.material.themeFAB(fab) + fab.setOnClickListener { showCreateBottomSheet(fileActivity) } + } + + private fun showCreateBottomSheet(fileActivity: FileActivity) { + val currentDir = currentFile + if (currentDir == null) { + Log_OC.w(TAG, "Cannot open create bottom sheet: current folder is unknown") + 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) { + 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() + } + fab.slideHideBottomBehavior(visible) + } + } + + fun setFabEnabled(enabled: Boolean) { + val fab = fabMain ?: return + val activity = activity ?: return + activity.runOnUiThread { + fab.isEnabled = enabled + viewThemeUtils.material.themeFAB(fab) + } + } + + private fun setBottomNavigationVisible(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) + //endregion + + //region Create and upload actions + override fun createFolder(encrypted: Boolean) { + if (encrypted) { + encryptedClickHandler.onNewEncryptedFolder() + return + } + + showCreateFolderDialog(false) + } + + fun showCreateFolderDialog(encrypted: Boolean) { + val activity = activity + if (activity == null) { + Log_OC.e(TAG, "Cannot show create folder dialog: fragment is not attached") + return + } + + CreateFolderDialogFragment.newInstance(currentFile, encrypted) + .show(activity.supportFragmentManager, DIALOG_CREATE_FOLDER) + } + + @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 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 + } + + if (!PermissionUtil.checkSelfPermission(fileDisplayActivity, Manifest.permission.CAMERA)) { + PermissionUtil.requestCameraPermission(fileDisplayActivity, PermissionUtil.PERMISSIONS_CAMERA) + return + } + + showCameraUploadTypeDialog(fileDisplayActivity) + } + + private fun showCameraUploadTypeDialog(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) { _, _ -> + 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 + ) + } + + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(fileDisplayActivity, builder) + builder.show() + } + + override fun scanDocUpload() { + val fileDisplayActivity = activity as? FileDisplayActivity + val currentFile = currentFile + if (fileDisplayActivity == null || currentFile == null || !currentFile.isFolder) { + Log_OC.w( + TAG, + "Cannot start document scan: activity=${activity?.javaClass?.simpleName}, " + + "folder=${currentFile?.remotePath}, isFolder=${currentFile?.isFolder}" + ) + 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) + } + + @Suppress("DEPRECATION") + override fun scanDocUploadFromApp() { + requireActivity().startActivityForResult( + scanIntentExternalApp, + FileDisplayActivity.REQUEST_CODE__SELECT_CONTENT_FROM_APPS_AUTO_RENAME + ) + } + + override val isScanDocUploadFromAppAvailable: Boolean + get() { + val context = activity ?: return false + return scanIntentExternalApp.resolveActivity(context.packageManager) != null + } + + override fun uploadFiles() { + val fileActivity = activity as? FileActivity + if (fileActivity == null) { + 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, "Cannot upload files: no user is logged in") + return + } + + val folder = currentFile + if (folder == null) { + Log_OC.w(TAG, "Cannot upload files: current folder is unknown") + return + } + + UploadFilesActivity.startUploadActivityForResult( + fileActivity, + user, + FileDisplayActivity.REQUEST_CODE__SELECT_FILES_FROM_FILE_SYSTEM, + folder.isEncrypted + ) + } + + override fun createRichWorkspace() { + val fileActivity = activity as? FileActivity ?: return + val remotePath = currentFile?.remotePath ?: return + + fileActivity.filesRepository.createRichWorkspace( + remotePath, + onCompleted = { url -> + containerActivity?.fileOperationsHelper + ?.openRichWorkspaceWithTextEditor(currentFile, url, requireContext()) + }, + onError = { + Log_OC.e(TAG, "Cannot create rich workspace for $remotePath") + SnackbarUtil.show(view, R.string.failed_to_start_editor) + } + ) + } + + override fun newDocument() = + showRichDocumentsTemplateDialog(ChooseRichDocumentsTemplateDialogFragment.Type.DOCUMENT) + + override fun newSpreadsheet() = + showRichDocumentsTemplateDialog(ChooseRichDocumentsTemplateDialogFragment.Type.SPREADSHEET) + + override fun newPresentation() = + showRichDocumentsTemplateDialog(ChooseRichDocumentsTemplateDialogFragment.Type.PRESENTATION) + + private fun showRichDocumentsTemplateDialog(type: ChooseRichDocumentsTemplateDialogFragment.Type) { + ChooseRichDocumentsTemplateDialogFragment.newInstance(currentFile, type) + .show(requireActivity().supportFragmentManager, DIALOG_CREATE_DOCUMENT) + } + + override fun showTemplate(creator: Creator?, headline: String?) { + 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) + } + + override fun showShareDetailView(file: OCFile) { + containerActivity?.showDetails(file, SHARING_TAB) + } + + override fun showActivityDetailView(file: OCFile) { + containerActivity?.showDetails(file, ACTIVITY_TAB) + } + + override fun onOverflowIconClicked(file: OCFile, view: View?) { + openActionsMenu(SINGLE_SELECTION, hashSetOf(file), true) + } + + override fun onHeaderClicked() { + 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(folder) + } + + override fun onLongItemClicked(file: OCFile): Boolean { + val activity = activity ?: return true + + if (activeActionMode != null) { + toggleFileSelection(file) + return true + } + + activity.startActionMode(multiChoiceModeListener) + commonAdapter?.addCheckedFile(file) + multiChoiceModeListener.onSelectionChanged(file) + return true + } + + override fun onItemClicked(file: OCFile) { + onItemClicked(file, null) + } + + @OptIn(markerClass = [UnstableApi::class]) + override fun onItemClicked(file: OCFile?, sourceView: View?) { + if (file == null) { + Log_OC.w(TAG, "Ignoring click on a null list item") + return + } + + val commonAdapter = commonAdapter + when { + commonAdapter?.isMultiSelect() == true -> toggleFileSelection(file) + commonAdapter != null && file.isFolder -> onFolderClicked(file, commonAdapter.getItemPosition(file)) + fileSelectable -> finishWithSelectedFile(file) + !onlyFoldersClickable -> onFileClicked(file, sourceView) + } + } + + 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() + } + } + + private fun onFolderClicked(folder: OCFile, position: Int) { + val folderPickerActivity = requireActivity() as? FolderPickerActivity + if (folderPickerActivity != null) { + val filenameErrorMessage = FileNameValidator.checkFileName(folder.fileName, capabilities, requireContext()) + if (filenameErrorMessage != null) { + SnackbarUtil.show(folderPickerActivity, filenameErrorMessage) + return + } + } + + if (folder.isEncrypted) { + encryptedClickHandler.onEncryptedFolderClick(folder, position) + return + } + + isSearchFragment = false + setEmptyListMessage(EmptyListState.LOADING) + browseToFolder(folder, position) + } + + private fun onFileClicked(file: OCFile, sourceView: View?) { + val restrictionMessage = getOpenRestrictionMessage(file) + val recyclerView = recyclerView + if (recyclerView != null && restrictionMessage != null) { + Snackbar.make(recyclerView, restrictionMessage, Snackbar.LENGTH_LONG).show() + return + } + + val fileDisplayActivity = containerActivity as? FileDisplayActivity + when { + fileDisplayActivity != null && fileDisplayActivity.canPreviewInMediaPager(file) -> + fileDisplayActivity.previewImageWithSearchContext(file, isSearchFragment, currentSearchType, sourceView) + + file.isDown && fileDisplayActivity != null -> + fileDisplayActivity.previewFile( + file, + object : CompletionCallback { + override fun onComplete(value: Boolean) = setFabVisible(value) + } + ) + + else -> openFile(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 + } + + private fun openFile(file: OCFile) { + if (!isAccountManagerInitialized()) { + Log_OC.e(TAG, "Cannot open ${file.remotePath}: account manager is not initialized") + return + } + + val context = context + if (context == null) { + Log_OC.e(TAG, "Cannot open ${file.remotePath}: fragment is not attached") + return + } + + val webViewAvailable = context.isWebViewAvailable() + val fileDisplayActivity = containerActivity as? FileDisplayActivity + + when { + !file.isEncrypted && fileDisplayActivity != null && fileDisplayActivity.canPreviewInAudioPlayer(file) -> { + setFabVisible(false) + fileDisplayActivity.startAudioPreview(file, true, true) + } + + webViewAvailable && hasTextEditor(file) && !file.isEncrypted -> + TextEditorWebView.startTextEditor(file, context) + + canOpenAsRichDocument(file, webViewAvailable) -> + containerActivity?.fileOperationsHelper?.openFileAsRichDocument(file, context) + + fileDisplayActivity != null -> { + fileDisplayActivity.startDownloadForPreview(file, currentFile) + if (file.isFileEligibleForImmediatePreview) { + fileDisplayActivity.setFileIDForImmediatePreview(file.fileId) + } + } + } + } + + private fun hasTextEditor(file: OCFile): Boolean = editorUtils.getEditor(accountManager.user, file.mimeType) != null + + private fun canOpenAsRichDocument(file: OCFile, webViewAvailable: Boolean): Boolean { + if (!webViewAvailable) return false + + val user = accountManager.user + if (user.isAnonymous) return false + + val capability = containerActivity?.storageManager?.getCapability(user.accountName) ?: return false + val mimeTypes = capability.richDocumentsMimeTypeList ?: return false + + return file.mimeType in mimeTypes && + capability.richDocumentsDirectEditing.isTrue && + !file.isEncrypted + } + //endregion + + //region Selection + private fun toggleFileSelection(file: OCFile) { + commonAdapter?.run { + if (isCheckedFile(file)) removeCheckedFile(file) else addCheckedFile(file) + } + multiChoiceModeListener.onSelectionChanged(file) + } + + @SuppressLint("NotifyDataSetChanged") + fun selectAllFiles(select: Boolean) { + val adapter = recyclerView?.adapter + if (adapter !is CommonOCFileListAdapterInterface) return + + adapter.selectAll(select) + adapter.notifyDataSetChanged() + activeActionMode?.invalidate() + } + + fun exitSelectionMode() { + activeActionMode?.finish() + } + //endregion + + //region File actions + 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 + + 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) + } + } + + private fun showActionsBottomSheet(bottomSheet: FileActionsBottomSheet, checkedFiles: Set) { + bottomSheet.setResultListener(childFragmentManager, this) { id -> onFileActionChosen(id, checkedFiles) } + if (isDialogFragmentReady()) { + bottomSheet.show(childFragmentManager, ACTIONS_BOTTOM_SHEET_TAG) + } + } + + open fun onFileActionChosen(@IdRes itemId: Int, checkedFiles: Set): Boolean { + if (checkedFiles.isEmpty()) return false + if (handleListAction(itemId, checkedFiles)) 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() + } + + return isHandled + } + + 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 -> { + syncFilesIfSpaceAvailable(checkedFiles) + exitSelectionMode() + } + + itemId == R.id.action_move_or_copy -> startMoveOrCopy(checkedFiles) + + itemId == R.id.action_select_all_action_menu -> selectAllFiles(true) + + itemId == R.id.action_deselect_all_action_menu -> selectAllFiles(false) + + itemId == R.id.action_send_file -> containerActivity?.fileOperationsHelper?.sendFiles(checkedFiles) + + else -> return false + } + + return true + } + + private fun showDetailsAndHideSortGroup(file: OCFile) { + activeActionMode?.finish() + containerActivity?.run { + showDetails(file) + showSortListGroup(false) + } + } + + private fun startMoveOrCopy(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 (!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 + } + + openFolderPickerForMoveOrCopy(checkedFiles) + } + + private fun findInvalidFilename(files: Set): String? = files.firstNotNullOfOrNull { + FileNameValidator.checkFileName(it.fileName, capabilities, requireContext()) + } + + @Suppress("DEPRECATION") + 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) + 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 syncFilesIfSpaceAvailable(files: Collection) { + if (files.isEmpty()) return + + (containerActivity as? FileActivity)?.showSyncLoadingDialog(files.any { it.isFolder }) + + val lastIndex = files.size - 1 + files.forEachIndexed { index, file -> + if (FileStorageUtils.checkIfEnoughSpace(file)) { + containerActivity?.fileOperationsHelper?.syncFileOrFolder(file, index == lastIndex, false) + } else { + Log_OC.w(TAG, "Not enough space to sync ${file.remotePath}") + showNotEnoughSpaceDialog(file, FileOperationsHelper.getAvailableSpaceOnDevice()) + } + } + } + + private fun showNotEnoughSpaceDialog(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) + } + //endregion + + //region Navigation + fun onBrowseUp(): Int { + val folder = currentFile + if (folder == null || folder.isRootDirectory) return 0 + + 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 + } + + Log_OC.d(TAG, "Browsed up $levelsUp level(s) from ${folder.remotePath} to ${parent.remotePath}") + currentFile = parent + setFileDepth(parent) + + if (parent.isRootDirectory && currentSearchType != SearchType.NO_SEARCH && activeSearchEvent != null) { + isSearchFragment = true + } + + reloadAndRestorePosition() + return levelsUp + } + + private fun reloadAndRestorePosition() { + listDirectory(currentFile, MainApp.isOnlyOnDevice()) + onRefresh(false) + restoreIndexAndTopPosition() + } + + fun browseToFolder(folder: OCFile?, position: Int) { + setFileDepth(folder) + + if (currentSearchType == SearchType.FAVORITE_SEARCH) { + resetSortAndGridButtons() + } + + listDirectory(folder, MainApp.isOnlyOnDevice()) + containerActivity?.onBrowsedDownTo(folder) + saveIndexAndTopPosition(position) + } + + private fun browseToRoot() { + val root = requireContainerActivity().storageManager.getFileByEncryptedRemotePath(OCFile.ROOT_PATH) + browseToFolder(root, FIRST_POSITION) + } + + fun setFileDepth(file: OCFile?) { + currentFileDepth = file.getDepth() + } + + fun resetFileDepth() { + currentFileDepth = OCFileDepth.Root + } + + val fileDepth: OCFileDepth? + get() = currentFileDepth + //endregion + + //region Directory listing + fun listDirectory(onlyOnDevice: Boolean) { + listDirectory(null, onlyOnDevice) + } + + fun listDirectory(directory: OCFile?, onlyOnDevice: Boolean) { + listDirectory(directory, null, onlyOnDevice) + } + + fun listDirectory(directory: OCFile?, highlightedFile: OCFile?, onlyOnDevice: Boolean) { + if (isSearchFragment) { + activeSearchEvent?.let { + startSearch(it) + mRefreshListLayout?.isRefreshing = false + } + return + } + + val storageManager = containerActivity?.storageManager + if (storageManager == null) { + Log_OC.w(TAG, "Cannot list directory: storage manager is not available") + return + } + + val directoryToList = resolveDirectoryToList(directory, storageManager) + if (directoryToList == null) { + 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, "Cannot list directory: list is not set up yet") + return + } + + val adapter = adapter + if (adapter == null) { + Log_OC.w(TAG, "Cannot list directory: adapter is not set up yet") + return + } + + Log_OC.i(TAG, "Listing directory ${directoryToList.decryptedRemotePath}, onlyOnDevice=$onlyOnDevice") + adapter.swapDirectory(accountManager.user, directoryToList, storageManager, onlyOnDevice, mimeType) + + val previousDirectory = currentFile + currentFile = directoryToList + updateUiForCurrentFolder() + scrollToHighlightedFileOrTop(adapter, highlightedFile, previousDirectory, directoryToList) + } + + private fun scrollToHighlightedFileOrTop( + adapter: OCFileListAdapter, + highlightedFile: OCFile?, + previousDirectory: OCFile?, + listedDirectory: OCFile + ) { + val recyclerView = recyclerView + + if (highlightedFile != null) { + adapter.setHighlightedItem(highlightedFile) + val position = adapter.getItemPosition(highlightedFile) + if (position != RecyclerView.NO_POSITION && recyclerView != null) { + recyclerView.scrollToPosition(position) + } + return + } + + if (recyclerView != null && previousDirectory != listedDirectory) { + recyclerView.scrollToPosition(FIRST_POSITION) + } + } + + private fun resolveDirectoryToList(directory: OCFile?, storageManager: FileDataStorageManager): OCFile? { + val candidate = directory ?: currentFile ?: storageManager.getFileByPath(OCFile.ROOT_PATH) ?: return null + if (candidate.isFolder) return 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 folder = currentFile ?: return + setFabVisible(canCreateInside(folder)) + listDirectory(folder, MainApp.isOnlyOnDevice()) + } + + val adapterFiles: List + get() = adapter?.files.orEmpty() + + fun updateOCFile(file: OCFile) { + adapter?.replaceFile(file) + } + + val isEmpty: Boolean + get() = adapter?.isEmpty ?: true + + private fun updateUiForCurrentFolder() { + fileListLayoutManager?.setLayoutViewMode() + updateSortButtonLabel() + setLayoutSwitchButton() + + val folder = currentFile + val readOnly = isReadOnlyFolder(folder) + setFabVisible(!hideFab && !readOnly) + setBottomNavigationVisible(!hideFab) + setFabEnabled(folder != null && !readOnly && (folder.canCreateFileAndFolder() || folder.isOfflineOperation)) + + activeActionMode?.invalidate() + } + + private fun updateSortButtonLabel() { + 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)) + } + //endregion + + //region Menu + @Deprecated("Deprecated in Java") + override fun onPrepareOptionsMenu(menu: Menu) { + if (menuItemAddRemoveValue == MenuItemAddRemove.REMOVE_GRID_AND_SORT) { + menu.removeItem(R.id.action_search) + } + + if (currentSearchType == SearchType.FAVORITE_SEARCH) { + resetSortAndGridButtons() + } else { + updateSortAndGridButtons() + } + } + + private fun updateSortAndGridButtons() { + val switchGridViewButton = mSwitchGridViewButton ?: return + val sortButton = mSortButton ?: return + + when (menuItemAddRemoveValue) { + MenuItemAddRemove.ADD_GRID_AND_SORT_WITH_SEARCH -> { + switchGridViewButton.visibility = View.VISIBLE + sortButton.visibility = View.VISIBLE + } + + MenuItemAddRemove.REMOVE_SORT -> sortButton.visibility = View.GONE + + MenuItemAddRemove.REMOVE_GRID_AND_SORT -> { + sortButton.visibility = View.GONE + switchGridViewButton.visibility = View.GONE + } + + MenuItemAddRemove.DO_NOTHING -> Unit + } + } + + private fun resetSortAndGridButtons() { + menuItemAddRemoveValue = MenuItemAddRemove.ADD_GRID_AND_SORT_WITH_SEARCH + updateSortAndGridButtons() + } + + val menuItemId: Int + get() = when { + javaClass == GalleryFragment::class.java -> R.id.nav_gallery + + javaClass == SharedListFragment::class.java || + isSearchEventShared || + currentSearchType == SearchType.SHARED_FILTER -> R.id.nav_shared + + javaClass == GroupfolderListFragment::class.java || + currentSearchType == SearchType.GROUPFOLDER -> R.id.nav_groupfolders + + isSearchEventFavorite || currentSearchType == SearchType.FAVORITE_SEARCH -> R.id.nav_favorites + + currentSearchType == SearchType.RECENT_FILES_SEARCH -> R.id.nav_recent_files + + else -> R.id.nav_all_files + } + //endregion + + //region Search + protected fun applySearchEvent(event: SearchEvent?) { + if (event == null || !event.hasQueryOrFilter()) return + updateSearchType(event) + updateMenuForSearch(event) + } + + fun updateSearchType(event: SearchEvent) { + event.toSearchType()?.let { currentSearchType = it } + } + + private fun updateMenuForSearch(event: SearchEvent) { + if (event.searchType == RemoteSearchType.FAVORITE_SEARCH || + event.searchType == RemoteSearchType.RECENTLY_MODIFIED_SEARCH + ) { + menuItemAddRemoveValue = MenuItemAddRemove.REMOVE_SORT + } + + if (currentSearchType != SearchType.FILE_SEARCH) { + activity?.invalidateOptionsMenu() + } + } + + private fun showEmptyStateForSearch(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) + } + + fun resetSearchAttributes() { + isSearchFragment = false + searchEvent = null + currentSearchType = SearchType.NO_SEARCH + } + + private fun startSearch(event: SearchEvent) { + if (event.searchType == RemoteSearchType.PHOTO_SEARCH) return + + if (searchTask?.isFinished() == false) { + 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() + setEmptyListMessage(EmptyListState.LOADING) + } + } + + applySearchEvent(event) + isSearchFragment = true + setFabVisible(false) + + mainHandler.post { + updateSortButtonLabel() + fileListLayoutManager?.setLayoutViewMode() + } + + launchSearchTask(event) + } + + private fun launchSearchTask(event: SearchEvent) { + val currentUser = accountManager.user + val remoteOperation = if (currentSearchType == SearchType.RECENT_FILES_SEARCH) { + createRecentFilesSearchOperation() + } else { + getSearchRemoteOperation(currentUser, event) + } + + 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() } + } + + 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) + } + + private fun createRecentFilesSearchOperation(): RemoteOperation<*> { + val capability = capabilities + val nowSeconds = System.currentTimeMillis() / AppConstants.MILLIS_PER_SECOND + + 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 + } + } + + override fun onRefresh() { + val event = activeSearchEvent + if (isSearchFragment && event != null) { + startSearch(event) + mRefreshListLayout?.isRefreshing = false + return + } + + isSearchFragment = false + super.onRefresh() + } + + fun restartSearch() { + searchTask?.cancel() + searchTask = null + searchEvent?.let { startSearch(it) } + } + + val isSearchEventFavorite: Boolean + get() = searchEvent?.searchType == RemoteSearchType.FAVORITE_SEARCH + + val isSearchEventShared: Boolean + get() = searchEvent?.searchType == RemoteSearchType.SHARED_FILTER + + fun shouldNavigateBackToAllFiles(): Boolean = + this is GalleryFragment || isSearchEventFavorite || isSearchEventShared + + private fun SearchEvent.hasQueryOrFilter(): Boolean = + searchQuery.isNotEmpty() || searchType in SEARCH_TYPES_WITHOUT_QUERY + //endregion + + //region Event bus + @Subscribe(threadMode = ThreadMode.MAIN) + fun onChangeMenuEvent(event: ChangeMenuEvent) { + Log_OC.d(TAG, "Leaving search, resetting search and menu state") + + arguments?.clear() + resetSearchAttributes() + resetSortAndGridButtons() + + (activity as? FileDisplayActivity)?.run { + invalidateOptionsMenu() + intent.removeExtra(SEARCH_EVENT) + setupHomeSearchToolbarWithSortAndListButtons() + updateActionBarTitleAndHomeButton(null) + } + + currentFile?.let { setFabVisible(canCreateInside(it)) } + setBottomNavigationVisible(true) + } + + @Subscribe(threadMode = ThreadMode.BACKGROUND) + fun onCommentsEvent(event: CommentsEvent) { + adapter?.markCommentsAsRead(event.remoteId) + } + + @Subscribe(threadMode = ThreadMode.MAIN) + 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 + } + + val result = withContext(Dispatchers.IO) { + ToggleFavoriteRemoteOperation(event.shouldFavorite, event.remotePath).execute(client) + } + if (!result.isSuccess) { + Log_OC.e(TAG, "Cannot toggle favorite for ${event.remotePath}: ${result.code}") + return@launch + } + + showFavoriteChange(event) + } + } + + 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) + } + } + + @Subscribe(threadMode = ThreadMode.BACKGROUND) + fun onSearchEvent(event: SearchEvent) { + startSearch(event) + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onEncryptionEvent(event: EncryptionEvent) { + event.onResult(this) + } + + @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 + } + + val result = withContext(Dispatchers.IO) { + ToggleFileLockRemoteOperation(event.shouldLock, event.filePath).execute(client) + } + if (!result.isSuccess) { + Log_OC.e(TAG, "Cannot toggle lock for ${event.filePath}: ${result.code}") + showFileLockError() + return@launch + } + + onRefresh() + } + } + + 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 + + private val MY_PACKAGE = OCFileListFragment::class.java.`package`?.name ?: "com.owncloud.android.ui.fragment" + + 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" + + const val DOWNLOAD_SEND = "DOWNLOAD_SEND" + + const val FOLDER_LAYOUT_LIST = "LIST" + const val FOLDER_LAYOUT_GRID = "GRID" + + 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 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" + + private const val ALL_MIME_TYPES = "*/*" + private const val SCAN_TO_PDF_ACTION = "org.fairscan.app.action.SCAN_TO_PDF" + + private const val ACTIVITY_TAB = 0 + private const val SHARING_TAB = 1 + private const val FIRST_POSITION = 0 + private const val SINGLE_SELECTION = 1 + + private const val RECENT_FILES_MAX_AGE_DAYS = 14L + private const val RECENT_FILES_SEARCH_LIMIT = 100 + + 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 val SEARCH_TYPES_WITHOUT_QUERY = setOf( + RemoteSearchType.SHARED_FILTER, + RemoteSearchType.FAVORITE_SEARCH, + RemoteSearchType.RECENTLY_MODIFIED_SEARCH + ) + + private val scanIntentExternalApp = Intent(SCAN_TO_PDF_ACTION) + + private var currentFileDepth: OCFileDepth? = OCFileDepth.Root + + var isMultipleFileSelectedForCopyOrMove = false + } +} 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..df00d14c251b --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListMultiChoiceModeListener.kt @@ -0,0 +1,176 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * 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" + } +} 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);