From 79a6424d74a5cfc168ec109ff16b03e245e461be Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 29 Oct 2026 09:09:00 +0300 Subject: [PATCH 1/3] refactor Signed-off-by: alperozturk96 --- .../ui/activity/FileDisplayActivity.kt | 2252 ++++++++--------- 1 file changed, 989 insertions(+), 1263 deletions(-) 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 451ac1d9bb0f..fd75cf37a1fa 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 @@ -20,7 +20,6 @@ import android.annotation.SuppressLint import android.content.BroadcastReceiver import android.content.ComponentName import android.content.Context -import android.content.DialogInterface import android.content.Intent import android.content.IntentFilter import android.content.ServiceConnection @@ -35,7 +34,6 @@ import android.os.Handler import android.os.IBinder import android.os.Looper import android.os.Parcelable -import android.text.TextUtils import android.view.Menu import android.view.MenuItem import android.view.View @@ -47,9 +45,9 @@ import androidx.activity.viewModels import androidx.annotation.VisibleForTesting import androidx.appcompat.widget.SearchView import androidx.core.util.Function -import androidx.core.view.MenuItemCompat import androidx.core.view.ViewCompat import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope @@ -100,7 +98,6 @@ import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.datamodel.VirtualFolderType -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 @@ -153,7 +150,6 @@ import com.owncloud.android.ui.fragment.albums.AlbumOperationListener import com.owncloud.android.ui.fragment.albums.AlbumsFragment import com.owncloud.android.ui.helpers.FileOperationsHelper import com.owncloud.android.ui.helpers.UriUploader -import com.owncloud.android.ui.interfaces.TransactionInterface import com.owncloud.android.ui.navigation.NavigatorScreen import com.owncloud.android.ui.navigation.animator.NavigationAnimator import com.owncloud.android.ui.preview.media.PreviewMediaActivity @@ -161,7 +157,7 @@ import com.owncloud.android.ui.preview.media.PreviewMediaFragment import com.owncloud.android.ui.preview.text.PreviewTextFileFragment import com.owncloud.android.ui.preview.text.PreviewTextFragment import com.owncloud.android.ui.preview.text.PreviewTextStringFragment -import com.owncloud.android.ui.preview.pdf.PreviewPdfFragment.Companion.newInstance +import com.owncloud.android.ui.preview.pdf.PreviewPdfFragment import com.owncloud.android.utils.DataHolderUtil import com.owncloud.android.utils.ErrorMessageAdapter import com.owncloud.android.utils.FileSortOrder @@ -182,19 +178,12 @@ import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import java.io.File import java.lang.ref.WeakReference -import java.util.function.Supplier import javax.inject.Inject /** * Displays, what files the user has available in his Nextcloud. This is the main view. */ -@Suppress( - "ComplexCondition", - "SpreadOperator", - "ForbiddenComment", - "LargeClass", - "NestedBlockDepth" -) +@Suppress("ComplexCondition", "ForbiddenComment", "LargeClass") class FileDisplayActivity : FileActivity(), FileFragment.ContainerActivity, @@ -215,7 +204,7 @@ class FileDisplayActivity : var mLastSslUntrustedServerResult: RemoteOperationResult<*>? = null - private var mWaitingToPreview: OCFile? = null + private var waitingToPreview: OCFile? = null private var syncState: Parcelable = EmptyListState.LOADING set(value) { @@ -225,9 +214,9 @@ class FileDisplayActivity : private var pendingSyncFolderOperation: Runnable? = null - private var mWaitingToSend: OCFile? = null + private var waitingToSend: OCFile? = null - private var mDrawerMenuItemstoShowHideList: MutableCollection? = null + private var drawerMenuItemsToShowHide: List = emptyList() private var searchQuery: String? = "" private var searchOpen = false @@ -282,7 +271,7 @@ class FileDisplayActivity : * Indicates whether the downloaded file should be previewed immediately. Since `FileDownloadWorker` can be * triggered from multiple sources, this helps determine if an automatic preview is needed after download. */ - private var fileIDForImmediatePreview: Long = -1 + private var fileIDForImmediatePreview: Long = NO_IMMEDIATE_PREVIEW_FILE_ID private var isShowingDetailsFromPreview = false @@ -325,9 +314,7 @@ class FileDisplayActivity : } // Restoring after UI has been inflated. - if (savedInstanceState != null) { - showSortListGroup(savedInstanceState.getBoolean(KEY_IS_SORT_GROUP_VISIBLE)) - } + savedInstanceState?.let { showSortListGroup(it.getBoolean(KEY_IS_SORT_GROUP_VISIBLE)) } checkStoragePath() observeWorkerState() @@ -346,35 +333,33 @@ class FileDisplayActivity : override fun getMenuItemId(): Int = MainApp.getMenuItemId() ?: listOfFilesFragment?.menuItemId ?: R.id.nav_all_files private fun loadSavedInstanceState(savedInstanceState: Bundle?) { - if (savedInstanceState != null) { - mWaitingToPreview = - savedInstanceState.getParcelableArgument(KEY_WAITING_TO_PREVIEW, OCFile::class.java) - syncState = savedInstanceState.getParcelableArgument(KEY_SYNC_STATE, Parcelable::class.java) - ?: EmptyListState.LOADING - mWaitingToSend = savedInstanceState.getParcelableArgument(KEY_WAITING_TO_SEND, OCFile::class.java) - searchQuery = savedInstanceState.getString(KEY_SEARCH_QUERY) - searchOpen = savedInstanceState.getBoolean(KEY_IS_SEARCH_OPEN, false) - } else { - mWaitingToPreview = null + if (savedInstanceState == null) { + waitingToPreview = null syncState = EmptyListState.LOADING - mWaitingToSend = null + waitingToSend = null + return + } + + savedInstanceState.run { + waitingToPreview = getParcelableArgument(KEY_WAITING_TO_PREVIEW, OCFile::class.java) + syncState = getParcelableArgument(KEY_SYNC_STATE, Parcelable::class.java) ?: EmptyListState.LOADING + waitingToSend = getParcelableArgument(KEY_WAITING_TO_SEND, OCFile::class.java) + searchQuery = getString(KEY_SEARCH_QUERY) + searchOpen = getBoolean(KEY_IS_SEARCH_OPEN, false) } } private fun initLayout() { - // Inflate and set the layout view binding = FilesBinding.inflate(layoutInflater) - setContentView(binding.getRoot()) + setContentView(binding.root) } private fun initUI() { setupHomeSearchToolbarWithSortAndListButtons() - mMenuButton.setOnClickListener { v: View? -> openDrawer() } + mMenuButton.setOnClickListener { openDrawer() } findViewById(R.id.toolbar).setOnClickListener { openCurrentFolderActions() } - mSwitchAccountButton.setOnClickListener { v: View? -> showManageAccountsDialog() } - mNotificationButton.setOnClickListener { - pushFragment(NavigatorScreen.Notifications) - } + mSwitchAccountButton.setOnClickListener { showManageAccountsDialog() } + mNotificationButton.setOnClickListener { pushFragment(NavigatorScreen.Notifications) } fastScrollUtils.fixAppBarForFastScroll(binding.appbar.appbar, binding.rootLayout) // reset ui states when file display activity created/recrated @@ -383,60 +368,54 @@ class FileDisplayActivity : private fun initTaskRetainerFragment() { // Init Fragment without UI to retain AsyncTask across configuration changes - val fm = supportFragmentManager - var taskRetainerFragment = - fm.findFragmentByTag(TaskRetainerFragment.FTAG_TASK_RETAINER_FRAGMENT) as TaskRetainerFragment? - if (taskRetainerFragment == null) { - taskRetainerFragment = TaskRetainerFragment() - fm.beginTransaction().add(taskRetainerFragment, TaskRetainerFragment.FTAG_TASK_RETAINER_FRAGMENT).commit() - } // else, Fragment already created and retained across configuration change + val tag = TaskRetainerFragment.FTAG_TASK_RETAINER_FRAGMENT + if (supportFragmentManager.findFragmentByTag(tag) is TaskRetainerFragment) return + + supportFragmentManager.beginTransaction().add(TaskRetainerFragment(), tag).commit() } private fun checkStoragePath() { val newStorage = Environment.getExternalStorageDirectory().absolutePath val storagePath = preferences.getStoragePath(newStorage) - if (!preferences.isStoragePathValid() && !File(storagePath).exists()) { - // falling back to default - preferences.setStoragePath(newStorage) - preferences.setStoragePathValid() - MainApp.setStoragePath(newStorage) + if (preferences.isStoragePathValid() || File(storagePath).exists()) return - try { - val builder = MaterialAlertDialogBuilder(this, R.style.Theme_ownCloud_Dialog) - .setTitle(R.string.wrong_storage_path) - .setMessage(R.string.wrong_storage_path_desc) - .setPositiveButton( - R.string.dialog_close - ) { dialog: DialogInterface?, which: Int -> dialog?.dismiss() } - .setIcon(R.drawable.ic_settings) + // falling back to default + preferences.setStoragePath(newStorage) + preferences.setStoragePathValid() + MainApp.setStoragePath(newStorage) + showWrongStoragePathDialog() + } - viewThemeUtils.dialog.colorMaterialAlertDialogBackground(applicationContext, builder) + private fun showWrongStoragePathDialog() { + try { + val builder = MaterialAlertDialogBuilder(this, R.style.Theme_ownCloud_Dialog) + .setTitle(R.string.wrong_storage_path) + .setMessage(R.string.wrong_storage_path_desc) + .setPositiveButton(R.string.dialog_close) { dialog, _ -> dialog?.dismiss() } + .setIcon(R.drawable.ic_settings) - builder.create().show() - } catch (e: BadTokenException) { - Log_OC.e(TAG, "Error showing wrong storage info, so skipping it: " + e.message) - } + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(applicationContext, builder) + + builder.create().show() + } catch (e: BadTokenException) { + Log_OC.e(TAG, "Error showing wrong storage info, so skipping it: ${e.message}") } } override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - val fragment = - supportFragmentManager.findFragmentByTag( - PermissionUtil.PERMISSION_CHOICE_DIALOG_TAG - ) as StoragePermissionDialogFragment? - if (fragment != null) { - val dialog = fragment.dialog - - if (dialog != null && dialog.isShowing) { - dialog.dismiss() - supportFragmentManager.beginTransaction().remove(fragment).commitNowAllowingStateLoss() - requestStoragePermissionIfNeeded(this) - } - } - } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return + + val fragment = supportFragmentManager + .findFragmentByTag(PermissionUtil.PERMISSION_CHOICE_DIALOG_TAG) as? StoragePermissionDialogFragment + ?: return + val dialog = fragment.dialog ?: return + if (!dialog.isShowing) return + + dialog.dismiss() + supportFragmentManager.beginTransaction().remove(fragment).commitNowAllowingStateLoss() + requestStoragePermissionIfNeeded(this) } override fun onPostCreate(savedInstanceState: Bundle?) { @@ -451,14 +430,8 @@ class FileDisplayActivity : requestStoragePermissionIfNeeded(this) } - if (intent.getParcelableArgument( - OCFileListFragment.SEARCH_EVENT, - SearchEvent::class.java - ) != null - ) { - switchToSearchFragment(savedInstanceState) - } else { - createMinFragments(savedInstanceState) + if (savedInstanceState == null) { + addInitialListOfFilesFragment() } if (savedInstanceState == null && intent.getBooleanExtra(EXTRA_RETURN_TO_PREVIEW, false)) { @@ -475,133 +448,91 @@ class FileDisplayActivity : */ private fun upgradeNotificationForInstantUpload() { // check for Android 6+ if legacy instant upload is activated --> disable + show info - if (preferences.instantPictureUploadEnabled() || preferences.instantVideoUploadEnabled()) { - preferences.removeLegacyPreferences() - // show info pop-up - MaterialAlertDialogBuilder(this, R.style.Theme_ownCloud_Dialog).setTitle(R.string.drawer_synced_folders) - .setMessage( - R.string.synced_folders_new_info - ).setPositiveButton( - R.string.drawer_open - ) { dialog: DialogInterface?, which: Int -> - // show instant upload - dialog?.dismiss() - pushFragment(NavigatorScreen.AutoUpload) - }.setNegativeButton( - R.string.drawer_close - ) { dialog: DialogInterface?, which: Int -> dialog?.dismiss() } - .setIcon( - R.drawable.nav_synced_folders - ).show() - } + if (!preferences.instantPictureUploadEnabled() && !preferences.instantVideoUploadEnabled()) return + + preferences.removeLegacyPreferences() + MaterialAlertDialogBuilder(this, R.style.Theme_ownCloud_Dialog) + .setTitle(R.string.drawer_synced_folders) + .setMessage(R.string.synced_folders_new_info) + .setPositiveButton(R.string.drawer_open) { dialog, _ -> + dialog?.dismiss() + pushFragment(NavigatorScreen.AutoUpload) + } + .setNegativeButton(R.string.drawer_close) { dialog, _ -> dialog?.dismiss() } + .setIcon(R.drawable.nav_synced_folders) + .show() } private fun checkOutdatedServer() { - val user = getUser() - val optionalCapability = capabilities - - // show outdated warning - if (user.isPresent && - optionalCapability.isPresent && - CapabilityUtils.checkOutdatedWarning( - getResources(), - user.get().server.version, - optionalCapability.get().extendedSupport.isTrue, - optionalCapability.get().hasValidSubscription.isTrue - ) - ) { + val currentUser = user.orElse(null) ?: return + val capability = capabilities.orElse(null) ?: return + + val isOutdated = CapabilityUtils.checkOutdatedWarning( + resources, + currentUser.server.version, + capability.extendedSupport.isTrue, + capability.hasValidSubscription.isTrue + ) + if (isOutdated) { SnackbarUtil.showServerOutdated(this, Snackbar.LENGTH_LONG) } } private fun checkNotifications() { - lifecycleScope.launch(Dispatchers.IO) { - try { - val result = GetNotificationsRemoteOperation() - .execute(clientFactory.createNextcloudClient(accountManager.user)) - - val isVisible = (result.isSuccess && result.getResultData()?.isEmpty() == false) - withContext(Dispatchers.Main) { - mNotificationButton.setVisibleIf(isVisible) - } - } catch (_: CreationException) { + lifecycleScope.launch { + val client = clientRepository.getNextcloudClient() ?: run { Log_OC.e(TAG, "Could not fetch notifications!") + return@launch + } + + val hasNotifications = withContext(Dispatchers.IO) { + val result = GetNotificationsRemoteOperation().execute(client) + result.isSuccess && result.resultData?.isEmpty() == false } + mNotificationButton.setVisibleIf(hasNotifications) } } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { when (requestCode) { - // handle notification permission on API level >= 33 - PermissionUtil.PERMISSIONS_POST_NOTIFICATIONS -> - // dialogue was dismissed -> prompt for storage permissions - requestStoragePermissionIfNeeded(this) - - // If request is cancelled, result arrays are empty. - PermissionUtil.PERMISSIONS_EXTERNAL_STORAGE -> - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - // permission was granted - EventBus.getDefault().post(TokenPushEvent()) - // toggle on is save since this is the only scenario this code gets accessed - } + // dialogue was dismissed -> prompt for storage permissions + PermissionUtil.PERMISSIONS_POST_NOTIFICATIONS -> requestStoragePermissionIfNeeded(this) - // If request is cancelled, result arrays are empty. - PermissionUtil.PERMISSIONS_CAMERA -> - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - // permission was granted - getOCFileListFragmentFromFile(object : TransactionInterface { - override fun onOCFileListFragmentComplete(fragment: OCFileListFragment) { - fragment.directCameraUpload() - } - }) - } + PermissionUtil.PERMISSIONS_EXTERNAL_STORAGE -> if (grantResults.isPermissionGranted()) { + EventBus.getDefault().post(TokenPushEvent()) + } + + PermissionUtil.PERMISSIONS_CAMERA -> if (grantResults.isPermissionGranted()) { + getOCFileListFragmentFromFile { it.directCameraUpload() } + } else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults) } } - private fun switchToSearchFragment(savedInstanceState: Bundle?) { - if (savedInstanceState == null) { - val listOfFiles = OCFileListFragment() - val args = Bundle() - - args.putParcelable( - OCFileListFragment.SEARCH_EVENT, - intent - .getParcelableArgument( - OCFileListFragment.SEARCH_EVENT, - SearchEvent::class.java - ) - ) - args.putBoolean(OCFileListFragment.ARG_ALLOW_CONTEXTUAL_ACTIONS, true) + // If request is cancelled, result arrays are empty. + private fun IntArray.isPermissionGranted(): Boolean = firstOrNull() == PackageManager.PERMISSION_GRANTED - listOfFiles.setArguments(args) - val transaction = supportFragmentManager.beginTransaction() - transaction.add(R.id.left_fragment_container, listOfFiles, TAG_LIST_OF_FILES) - transaction.commit() - } else { - supportFragmentManager.findFragmentByTag(TAG_LIST_OF_FILES) + private fun addInitialListOfFilesFragment() { + val searchEvent = intent.getParcelableArgument(OCFileListFragment.SEARCH_EVENT, SearchEvent::class.java) + val listOfFiles = OCFileListFragment().apply { + arguments = Bundle().apply { + searchEvent?.let { putParcelable(OCFileListFragment.SEARCH_EVENT, it) } + putBoolean(OCFileListFragment.ARG_ALLOW_CONTEXTUAL_ACTIONS, true) + } } - } - private fun createMinFragments(savedInstanceState: Bundle?) { - if (savedInstanceState == null) { - val listOfFiles = OCFileListFragment() - val args = Bundle() - args.putBoolean(OCFileListFragment.ARG_ALLOW_CONTEXTUAL_ACTIONS, true) - listOfFiles.setArguments(args) - val transaction = supportFragmentManager.beginTransaction() - transaction.add(R.id.left_fragment_container, listOfFiles, TAG_LIST_OF_FILES) - transaction.commit() + supportFragmentManager.beginTransaction() + .add(R.id.left_fragment_container, listOfFiles, TAG_LIST_OF_FILES) + .commit() + + if (searchEvent == null) { listFragmentJustCreated = true - } else { - supportFragmentManager.findFragmentByTag(TAG_LIST_OF_FILES) } } private fun initFragments() { - // First fragment - val listOfFiles = this.listOfFilesFragment + val listOfFiles = listOfFilesFragment if (listOfFiles != null && searchQuery.isNullOrEmpty()) { if (!listFragmentJustCreated) { listOfFiles.listDirectory(getCurrentDir(), file, MainApp.isOnlyOnDevice()) @@ -611,7 +542,6 @@ class FileDisplayActivity : Log_OC.e(TAG, "Still have a chance to lose the initialization of list fragment >(") } - // reset views resetScrollingAndUpdateActionBar() } @@ -639,26 +569,7 @@ class FileDisplayActivity : Intent.ACTION_SEARCH == action -> handleSearchIntent(intent) - ALL_FILES == action -> { - Log_OC.d(this, "Switch to oc file fragment") - - // Replace only if the fragment is NOT exactly OCFileListFragment - // Using `is OCFileListFragment` would also match subclasses, - // its needed because reinitializing OCFileListFragment itself causes an empty screen - leftFragment?.let { - // Album screens are committed under their own tags, so leftFragment can still - // point at the detached OCFileListFragment while an album screen is on top - if (it::class != OCFileListFragment::class || isAlbumsFragment || isAlbumItemsFragment) { - leftFragment = OCFileListFragment() - supportFragmentManager.executePendingTransactions() - listFragmentJustCreated = true - } - } - - // The onResume() that always follows this same-activity intent redelivery already - // lists and re-syncs the current directory, so doing it again here is redundant. - browseToRoot(performRefresh = false) - } + ALL_FILES == action -> switchToAllFiles() LIST_GROUPFOLDERS == action -> { Log_OC.d(this, "Switch to list groupfolders fragment") @@ -680,26 +591,41 @@ class FileDisplayActivity : } } + private fun switchToAllFiles() { + Log_OC.d(this, "Switch to oc file fragment") + + // Replace only if the fragment is NOT exactly OCFileListFragment + // Using `is OCFileListFragment` would also match subclasses, + // its needed because reinitializing OCFileListFragment itself causes an empty screen + val currentLeftFragment = leftFragment + // Album screens are committed under their own tags, so leftFragment can still + // point at the detached OCFileListFragment while an album screen is on top + if (currentLeftFragment != null && + (currentLeftFragment::class != OCFileListFragment::class || isAlbumsFragment || isAlbumItemsFragment) + ) { + leftFragment = OCFileListFragment() + supportFragmentManager.executePendingTransactions() + listFragmentJustCreated = true + } + + // The onResume() that always follows this same-activity intent redelivery already + // lists and re-syncs the current directory, so doing it again here is redundant. + browseToRoot(performRefresh = false) + } + @SuppressLint("UnsafeIntentLaunch") private fun handleCommonIntents(intent: Intent) { - if (passCodeManager.isPassCodeEnabled() && passCodeManager.isLocked(this)) { - return - } + if (passCodeManager.isPassCodeEnabled() && passCodeManager.isLocked(this)) return when (intent.action) { Intent.ACTION_VIEW -> handleOpenFileViaIntent(intent) - - OPEN_FILE -> { - onOpenFileIntent(intent) - } + OPEN_FILE -> onOpenFileIntent(intent) } } @SuppressLint("UnsafeIntentLaunch") private fun handleRestartIntent(intent: Intent) { - if (intent.action != RESTART) { - return - } + if (intent.action != RESTART) return finish() startActivity(intent) @@ -711,31 +637,28 @@ class FileDisplayActivity : SearchEvent::class.java ) ?: return - when (searchEvent.searchType) { + val searchFragment = when (searchEvent.searchType) { SearchRemoteOperation.SearchType.PHOTO_SEARCH -> { Log_OC.d(this, "Switch to photo search fragment") - leftFragment = GalleryFragment().apply { - arguments = searchEvent.getBundle() - } + GalleryFragment() } SearchRemoteOperation.SearchType.SHARED_FILTER -> { Log_OC.d(this, "Switch to shared fragment") - leftFragment = SharedListFragment().apply { - arguments = searchEvent.getBundle() - } + SharedListFragment() } else -> { Log_OC.d(this, "Switch to oc file search fragment") - leftFragment = OCFileListFragment().apply { - arguments = searchEvent.getBundle() - } + OCFileListFragment() } } + leftFragment = searchFragment.apply { arguments = searchEvent.getBundle() } - listOfFilesFragment?.isSearchFragment = true - listOfFilesFragment?.updateSearchType(searchEvent) + listOfFilesFragment?.run { + isSearchFragment = true + updateSearchType(searchEvent) + } } // endregion @@ -767,9 +690,7 @@ class FileDisplayActivity : } private fun setLeftFragment(fragment: Fragment?, showSortListGroup: Boolean) { - if (fragment == null) { - return - } + fragment ?: return isShowingDetailsFromPreview = false prepareFragmentBeforeCommit(showSortListGroup) @@ -803,39 +724,35 @@ class FileDisplayActivity : return true } - private fun getOCFileListFragmentFromFile(transaction: TransactionInterface) { - val leftFragment = this.leftFragment - - if (leftFragment is OCFileListFragment) { - transaction.onOCFileListFragmentComplete(leftFragment) + private fun getOCFileListFragmentFromFile(onFragmentReady: (OCFileListFragment) -> Unit) { + val currentLeftFragment = leftFragment + if (currentLeftFragment is OCFileListFragment) { + onFragmentReady(currentLeftFragment) return } - val listOfFiles = OCFileListFragment() - val args = Bundle() - args.putBoolean(OCFileListFragment.ARG_ALLOW_CONTEXTUAL_ACTIONS, true) - listOfFiles.setArguments(args) + val listOfFiles = OCFileListFragment().apply { + arguments = Bundle().apply { putBoolean(OCFileListFragment.ARG_ALLOW_CONTEXTUAL_ACTIONS, true) } + } runOnUiThread { - val fm = supportFragmentManager - if (!fm.isStateSaved && !fm.isDestroyed) { - prepareFragmentBeforeCommit(true) - if (commitFragment(listOfFiles)) { - fm.executePendingTransactions() - transaction.onOCFileListFragmentComplete(listOfFiles) - } + val fragmentManager = supportFragmentManager + if (fragmentManager.isStateSaved || fragmentManager.isDestroyed) return@runOnUiThread + + prepareFragmentBeforeCommit(true) + if (commitFragment(listOfFiles)) { + fragmentManager.executePendingTransactions() + onFragmentReady(listOfFiles) } } } fun showFileActions(file: OCFile?) { dismissLoadingDialog() - getOCFileListFragmentFromFile(object : TransactionInterface { - override fun onOCFileListFragmentComplete(fragment: OCFileListFragment) { - browseUp(fragment) - file?.let { fragment.onOverflowIconClicked(it, null) } - } - }) + getOCFileListFragmentFromFile { fragment -> + browseUp(fragment) + file?.let { fragment.onOverflowIconClicked(it, null) } + } } var leftFragment: Fragment? @@ -847,14 +764,9 @@ class FileDisplayActivity : } val listOfFilesFragment: OCFileListFragment? - get() { - val listOfFiles = - supportFragmentManager.findFragmentByTag(TAG_LIST_OF_FILES) - if (listOfFiles is OCFileListFragment) { - return listOfFiles - } + get() = supportFragmentManager.findFragmentByTag(TAG_LIST_OF_FILES) as? OCFileListFragment ?: run { Log_OC.e(TAG, "Access to unexisting list of files fragment") - return null + null } protected fun resetScrollingAndUpdateActionBar() { @@ -863,13 +775,11 @@ class FileDisplayActivity : } fun updateListOfFilesFragment() { - val fileListFragment = this.listOfFilesFragment - fileListFragment?.listDirectory(MainApp.isOnlyOnDevice()) + listOfFilesFragment?.listDirectory(MainApp.isOnlyOnDevice()) } fun resetSearchView() { - val fileListFragment = this.listOfFilesFragment - fileListFragment?.isSearchFragment = false + listOfFilesFragment?.isSearchFragment = false } protected fun refreshDetailsFragmentIfVisible( @@ -877,26 +787,26 @@ class FileDisplayActivity : downloadedRemotePath: String, success: Boolean ) { - val leftFragment = this.leftFragment as? FileDetailFragment ?: return - val fileInFragment = leftFragment.file + val detailFragment = leftFragment as? FileDetailFragment ?: return + val fileInFragment = detailFragment.file if (fileInFragment != null && downloadedRemotePath != fileInFragment.remotePath) { - mWaitingToPreview = null + waitingToPreview = null return } when (downloadEvent) { FileDownloadEventBroadcaster.ACTION_DOWNLOAD_ENQUEUED -> { - leftFragment.listenForTransferProgress() - leftFragment.updateFileDetails(true, false) + detailFragment.listenForTransferProgress() + detailFragment.updateFileDetails(true, false) } FileDownloadEventBroadcaster.ACTION_DOWNLOAD_COMPLETED -> { - val waitedPreview = mWaitingToPreview?.remotePath == downloadedRemotePath + val waitedPreview = waitingToPreview?.remotePath == downloadedRemotePath val previewStarted = waitedPreview && tryStartWaitingPreview(success) - mWaitingToPreview = null + waitingToPreview = null if (!previewStarted) { - leftFragment.updateFileDetails(false, success) + detailFragment.updateFileDetails(false, success) } } } @@ -907,8 +817,8 @@ class FileDisplayActivity : private fun tryStartWaitingPreview(success: Boolean): Boolean { if (!success) return false - mWaitingToPreview = mWaitingToPreview?.fileId?.let { storageManager.getFileById(it) } - val file = mWaitingToPreview ?: return false + waitingToPreview = waitingToPreview?.let { storageManager.getFileById(it.fileId) } + val file = waitingToPreview ?: return false return when { MimeTypeUtil.isVideo(file) -> { @@ -944,12 +854,8 @@ class FileDisplayActivity : } override fun onPrepareOptionsMenu(menu: Menu?): Boolean { - if (mDrawerMenuItemstoShowHideList != null) { - val drawerOpen = isDrawerOpen - for (menuItem in mDrawerMenuItemstoShowHideList) { - menuItem.isVisible = !drawerOpen - } - } + val drawerOpen = isDrawerOpen + drawerMenuItemsToShowHide.forEach { it.isVisible = !drawerOpen } menu?.findItem(R.id.action_current_folder_actions)?.isVisible = canShowCurrentFolderActions() @@ -970,12 +876,11 @@ class FileDisplayActivity : } override fun onCreateOptionsMenu(menu: Menu): Boolean { - val inflater = menuInflater - inflater.inflate(R.menu.activity_file_display, menu) + menuInflater.inflate(R.menu.activity_file_display, menu) menu.findItem(R.id.action_select_all).isVisible = false val searchMenuItem = menu.findItem(R.id.action_search) - searchView = MenuItemCompat.getActionView(searchMenuItem) as SearchView? + searchView = searchMenuItem.actionView as? SearchView searchMenuItem.isVisible = false mSearchText.setOnClickListener { showSearchView() @@ -985,149 +890,143 @@ class FileDisplayActivity : }, SEARCH_VIEW_FOCUS_DELAY) } - searchView?.let { viewThemeUtils.androidx.themeToolbarSearchView(it) } - // populate list of menu items to show/hide when drawer is opened/closed - mDrawerMenuItemstoShowHideList = ArrayList(1) - mDrawerMenuItemstoShowHideList?.add(searchMenuItem) + drawerMenuItemsToShowHide = listOf(searchMenuItem) - // focus the SearchView - if (!TextUtils.isEmpty(searchQuery)) { - searchView?.post { - searchView?.isIconified = false - searchView?.setQuery(searchQuery, true) - searchView?.clearFocus() - } - } + searchView?.let { setupSearchView(it) } - val mSearchEditFrame = searchView?.findViewById(androidx.appcompat.R.id.search_edit_frame) + return super.onCreateOptionsMenu(menu) + } - searchView?.setOnCloseListener { - if (TextUtils.isEmpty(searchView?.query.toString())) { - searchView?.onActionViewCollapsed() - setDrawerIndicatorEnabled(isDrawerIndicatorAvailable) // order matters - supportActionBar?.setDisplayHomeAsUpEnabled(true) - mDrawerToggle.syncState() + private fun setupSearchView(view: SearchView) { + viewThemeUtils.androidx.themeToolbarSearchView(view) - val ocFileListFragment = this.listOfFilesFragment - if (ocFileListFragment != null) { - ocFileListFragment.isSearchFragment = false - ocFileListFragment.refreshDirectory() - } - } else { - searchView?.post { searchView?.setQuery("", true) } - } + if (!searchQuery.isNullOrEmpty()) { + view.post { restoreSearchQuery() } + } + + view.setOnCloseListener { + onSearchViewClosed(view) true } - val vto = mSearchEditFrame?.viewTreeObserver - vto?.addOnGlobalLayoutListener(object : OnGlobalLayoutListener { - var oldVisibility: Int = -1 + view.findViewById(androidx.appcompat.R.id.search_edit_frame)?.let { + disableDrawerIndicatorWhenVisible(it) + } + } - override fun onGlobalLayout() { - val currentVisibility = mSearchEditFrame.visibility + private fun restoreSearchQuery() { + searchView?.run { + isIconified = false + setQuery(searchQuery, true) + clearFocus() + } + } - if (currentVisibility != oldVisibility) { - if (currentVisibility == View.VISIBLE) { - setDrawerIndicatorEnabled(false) - } + private fun onSearchViewClosed(view: SearchView) { + if (!view.query.isNullOrEmpty()) { + view.post { searchView?.setQuery("", true) } + return + } + + view.onActionViewCollapsed() + setDrawerIndicatorEnabled(isDrawerIndicatorAvailable) // order matters + supportActionBar?.setDisplayHomeAsUpEnabled(true) + mDrawerToggle.syncState() - oldVisibility = currentVisibility + listOfFilesFragment?.run { + isSearchFragment = false + refreshDirectory() + } + } + + private fun disableDrawerIndicatorWhenVisible(searchEditFrame: View) { + searchEditFrame.viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener { + var oldVisibility: Int = UNKNOWN_VISIBILITY + + override fun onGlobalLayout() { + val currentVisibility = searchEditFrame.visibility + if (currentVisibility == oldVisibility) return + + if (currentVisibility == View.VISIBLE) { + setDrawerIndicatorEnabled(false) } + oldVisibility = currentVisibility } }) - - return super.onCreateOptionsMenu(menu) } private fun shouldOpenDrawer(): Boolean = !isDrawerOpen && !isSearchOpen() && isRoot(getCurrentDir()) && - this.leftFragment is OCFileListFragment && + leftFragment is OCFileListFragment && !isAlbumItemsFragment /** * Called, when the user selected something for uploading */ - @Suppress("CyclomaticComplexMethod") override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (data != null && - ( - requestCode == REQUEST_CODE__SELECT_CONTENT_FROM_APPS || - requestCode == REQUEST_CODE__SELECT_CONTENT_FROM_APPS_AUTO_RENAME - ) && - (resultCode == RESULT_OK || resultCode == UploadFilesActivity.RESULT_OK_AND_MOVE) - ) { - requestUploadOfContentFromApps(requestCode, resultCode, data) - } else if (data != null && - requestCode == REQUEST_CODE__SELECT_FILES_FROM_FILE_SYSTEM && - ( - resultCode == RESULT_OK || - resultCode == UploadFilesActivity.RESULT_OK_AND_MOVE || - resultCode == UploadFilesActivity.RESULT_OK_AND_DO_NOTHING || - resultCode == UploadFilesActivity.RESULT_OK_AND_DELETE - ) - ) { - requestUploadOfFilesFromFileSystem(data, resultCode) - } else if (( - requestCode == REQUEST_CODE__UPLOAD_FROM_CAMERA || - requestCode == REQUEST_CODE__UPLOAD_FROM_VIDEO_CAMERA - ) && - (resultCode == RESULT_OK || resultCode == UploadFilesActivity.RESULT_OK_AND_DELETE) - ) { - CheckAvailableSpaceTask( - object : CheckAvailableSpaceListener { - override fun onCheckAvailableSpaceStart() { - Log_OC.d(this, "onCheckAvailableSpaceStart") - } + when { + data != null && requestCode in selectContentFromAppsRequestCodes && + resultCode in selectContentFromAppsResultCodes -> + requestUploadOfContentFromApps(requestCode, resultCode, data) - override fun onCheckAvailableSpaceFinish( - hasEnoughSpaceAvailable: Boolean, - vararg filesToUpload: String? - ) { - Log_OC.d(this, "onCheckAvailableSpaceFinish") - - if (hasEnoughSpaceAvailable) { - val file = File(filesToUpload[0]) - val renamedFile = if (requestCode == REQUEST_CODE__UPLOAD_FROM_CAMERA) { - File(file.parent + OCFile.PATH_SEPARATOR + FileOperationsHelper.getCapturedImageName()) - } else { - File(file.parent + OCFile.PATH_SEPARATOR + FileOperationsHelper.getCapturedVideoName()) - } - - if (!file.renameTo(renamedFile)) { - SnackbarUtil.show( - this@FileDisplayActivity, - R.string.error_uploading_direct_camera_upload - ) - return - } - - requestUploadOfFilesFromFileSystem( - renamedFile.parentFile?.absolutePath, - arrayOf(renamedFile.absolutePath), - FileUploadWorker.LOCAL_BEHAVIOUR_DELETE - ) - } - } - }, - *arrayOf( - FileOperationsHelper.createCameraFile( - this@FileDisplayActivity, - requestCode == REQUEST_CODE__UPLOAD_FROM_VIDEO_CAMERA - ).absolutePath - ) - ).execute() - } else if (requestCode == REQUEST_CODE__MOVE_OR_COPY_FILES && resultCode == RESULT_OK) { - exitSelectionMode() + data != null && requestCode == REQUEST_CODE__SELECT_FILES_FROM_FILE_SYSTEM && + resultCode in selectFilesFromFileSystemResultCodes -> + requestUploadOfFilesFromFileSystem(data, resultCode) + + requestCode in uploadFromCameraRequestCodes && resultCode in uploadFromCameraResultCodes -> + uploadFromCamera(requestCode) + + requestCode == REQUEST_CODE__MOVE_OR_COPY_FILES && resultCode == RESULT_OK -> exitSelectionMode() + + else -> super.onActivityResult(requestCode, resultCode, data) + } + } + + private fun uploadFromCamera(requestCode: Int) { + val isVideo = requestCode == REQUEST_CODE__UPLOAD_FROM_VIDEO_CAMERA + val cameraFilePath = FileOperationsHelper.createCameraFile(this, isVideo).absolutePath + + val listener = object : CheckAvailableSpaceListener { + override fun onCheckAvailableSpaceStart() { + Log_OC.d(this, "onCheckAvailableSpaceStart") + } + + override fun onCheckAvailableSpaceFinish(hasEnoughSpaceAvailable: Boolean, vararg filesToUpload: String?) { + Log_OC.d(this, "onCheckAvailableSpaceFinish") + if (!hasEnoughSpaceAvailable) return + + val capturedFilePath = filesToUpload.firstOrNull() ?: return + renameAndUploadCapturedFile(File(capturedFilePath), requestCode) + } + } + + CheckAvailableSpaceTask(listener, cameraFilePath).execute() + } + + private fun renameAndUploadCapturedFile(capturedFile: File, requestCode: Int) { + val capturedFileName = if (requestCode == REQUEST_CODE__UPLOAD_FROM_CAMERA) { + FileOperationsHelper.getCapturedImageName() } else { - super.onActivityResult(requestCode, resultCode, data) + FileOperationsHelper.getCapturedVideoName() } + val renamedFile = File(capturedFile.parent + OCFile.PATH_SEPARATOR + capturedFileName) + + if (!capturedFile.renameTo(renamedFile)) { + SnackbarUtil.show(this, R.string.error_uploading_direct_camera_upload) + return + } + + requestUploadOfFilesFromFileSystem( + renamedFile.parentFile?.absolutePath, + arrayOf(renamedFile.absolutePath), + FileUploadWorker.LOCAL_BEHAVIOUR_DELETE + ) } private fun exitSelectionMode() { - val ocFileListFragment = this.listOfFilesFragment - ocFileListFragment?.exitSelectionMode() + listOfFilesFragment?.exitSelectionMode() } private fun requestUploadOfFilesFromFileSystem(data: Intent, resultCode: Int) { @@ -1137,132 +1036,114 @@ class FileDisplayActivity : } private fun getRemotePaths(directory: String?, filePaths: Array, localBasePath: String): Array = - Array(filePaths.size) { j -> - val relativePath = StringUtils.removePrefix(filePaths[j], localBasePath) - (directory ?: "") + relativePath - } + filePaths.map { (directory ?: "") + StringUtils.removePrefix(it, localBasePath) }.toTypedArray() private fun requestUploadOfFilesFromFileSystem(localBasePath: String?, filePaths: Array, resultCode: Int) { - var localBasePath = localBasePath - if (localBasePath != null) { - if (!localBasePath.endsWith("/")) { - localBasePath = "$localBasePath/" - } - - val remotePathBase = getCurrentDir()?.remotePath - val decryptedRemotePaths = getRemotePaths(remotePathBase, filePaths, localBasePath) - - val behaviour = when (resultCode) { - UploadFilesActivity.RESULT_OK_AND_MOVE -> FileUploadWorker.LOCAL_BEHAVIOUR_MOVE - UploadFilesActivity.RESULT_OK_AND_DELETE -> FileUploadWorker.LOCAL_BEHAVIOUR_DELETE - else -> FileUploadWorker.LOCAL_BEHAVIOUR_FORGET - } - - connectivityService.isNetworkAndServerAvailable { result: Boolean? -> - if (result == true) { - val optionalCapabilities = capabilities - if (optionalCapabilities.isPresent) { - val isValidFolderPath = - remotePathBase?.let { - checkFolderPath( - it, - optionalCapabilities.get(), - this - ) - } - if (isValidFolderPath == false) { - SnackbarUtil.show( - this, - R.string.file_name_validator_error_contains_reserved_names_or_invalid_characters - ) - return@isNetworkAndServerAvailable - } - - val data = OCUploadLocalPathData.forFile( - user.orElseThrow(Supplier { RuntimeException() }), - filePaths, - decryptedRemotePaths, - behaviour, - createRemoteFolder = true - ) - - FileUploadHelper.instance().uploadNewFiles(data) - } - } else { - lifecycleScope.launch(Dispatchers.IO) { - fileDataStorageManager.addCreateFileOfflineOperation(filePaths, decryptedRemotePaths) - - withContext(Dispatchers.Main) { - refreshCurrentDirectory() - } - } - } - } - } else { + if (localBasePath == null) { Log_OC.d(TAG, "User clicked on 'Update' with no selection") SnackbarUtil.show(this, R.string.filedisplay_no_file_selected) + return } - } - private fun requestUploadOfContentFromApps(requestCode: Int, resultCode: Int, contentIntent: Intent) { - val streamsToUpload = ArrayList() + val normalizedLocalBasePath = if (localBasePath.endsWith("/")) localBasePath else "$localBasePath/" + val remotePathBase = getCurrentDir()?.remotePath + val decryptedRemotePaths = getRemotePaths(remotePathBase, filePaths, normalizedLocalBasePath) - if (contentIntent.clipData != null && (contentIntent.clipData?.itemCount ?: 0) > 0) { - for (i in 0..<(contentIntent.clipData?.itemCount ?: 0)) { - streamsToUpload.add(contentIntent.clipData?.getItemAt(i)?.uri) - } - } else { - streamsToUpload.add(contentIntent.data) + val behaviour = when (resultCode) { + UploadFilesActivity.RESULT_OK_AND_MOVE -> FileUploadWorker.LOCAL_BEHAVIOUR_MOVE + UploadFilesActivity.RESULT_OK_AND_DELETE -> FileUploadWorker.LOCAL_BEHAVIOUR_DELETE + else -> FileUploadWorker.LOCAL_BEHAVIOUR_FORGET } - val behaviour = - if (resultCode == - UploadFilesActivity.RESULT_OK_AND_MOVE - ) { - FileUploadWorker.LOCAL_BEHAVIOUR_MOVE + connectivityService.isNetworkAndServerAvailable { isAvailable: Boolean? -> + if (isAvailable == true) { + uploadNewFiles(remotePathBase, filePaths, decryptedRemotePaths, behaviour) } else { - FileUploadWorker.LOCAL_BEHAVIOUR_COPY + addCreateFileOfflineOperation(filePaths, decryptedRemotePaths) } + } + } - val currentDir = getCurrentDir() - val remotePath = if (currentDir != null) currentDir.remotePath else OCFile.ROOT_PATH - var fileDisplayNameTransformer: Function? = null - if (requestCode == REQUEST_CODE__SELECT_CONTENT_FROM_APPS_AUTO_RENAME) { - fileDisplayNameTransformer = { uri: Uri -> - val displayName = UriUtils.getDisplayNameForUri(uri, applicationContext) - if (displayName != null && displayName.isNotEmpty()) { - FileOperationsHelper.getTimestampedFileName("." + FilenameUtils.getExtension(displayName)) - } else { - null - } + private fun uploadNewFiles( + remotePathBase: String?, + filePaths: Array, + decryptedRemotePaths: Array, + behaviour: Int + ) { + val capability = capabilities.orElse(null) ?: return + + val isValidFolderPath = remotePathBase?.let { checkFolderPath(it, capability, this) } + if (isValidFolderPath == false) { + SnackbarUtil.show(this, R.string.file_name_validator_error_contains_reserved_names_or_invalid_characters) + return + } + + val currentUser = user.orElse(null) ?: return + val data = OCUploadLocalPathData.forFile( + currentUser, + filePaths, + decryptedRemotePaths, + behaviour, + createRemoteFolder = true + ) + + FileUploadHelper.instance().uploadNewFiles(data) + } + + private fun addCreateFileOfflineOperation(filePaths: Array, decryptedRemotePaths: Array) { + lifecycleScope.launch { + withContext(Dispatchers.IO) { + fileDataStorageManager.addCreateFileOfflineOperation(filePaths, decryptedRemotePaths) } + refreshCurrentDirectory() + } + } + + private fun requestUploadOfContentFromApps(requestCode: Int, resultCode: Int, contentIntent: Intent) { + val currentUser = user.orElse(null) ?: return + + val clipData = contentIntent.clipData + val streamsToUpload: List = if (clipData != null && clipData.itemCount > 0) { + List(clipData.itemCount) { clipData.getItemAt(it)?.uri } + } else { + listOf(contentIntent.data) } - val uploader = UriUploader( + val behaviour = if (resultCode == UploadFilesActivity.RESULT_OK_AND_MOVE) { + FileUploadWorker.LOCAL_BEHAVIOUR_MOVE + } else { + FileUploadWorker.LOCAL_BEHAVIOUR_COPY + } + + val remotePath = getCurrentDir()?.remotePath ?: OCFile.ROOT_PATH + val fileDisplayNameTransformer = if (requestCode == REQUEST_CODE__SELECT_CONTENT_FROM_APPS_AUTO_RENAME) { + Function { timestampedFileNameFor(it) } + } else { + null + } + + UriUploader( this, streamsToUpload, remotePath, - user.orElseThrow( - Supplier { RuntimeException() } - ), + currentUser, behaviour, false, // Not show waiting dialog while file is being copied from private storage null, // Not needed copy temp task listener fileDisplayNameTransformer - ) - - uploader.uploadUris() + ).uploadUris() } - fun isSearchOpen(): Boolean { - if (searchView == null) { - return false - } else { - val mSearchEditFrame = searchView?.findViewById(androidx.appcompat.R.id.search_edit_frame) - return mSearchEditFrame != null && mSearchEditFrame.isVisible - } + private fun timestampedFileNameFor(uri: Uri): String? { + val displayName = UriUtils.getDisplayNameForUri(uri, applicationContext) + if (displayName.isNullOrEmpty()) return null + + return FileOperationsHelper.getTimestampedFileName("." + FilenameUtils.getExtension(displayName)) } + fun isSearchOpen(): Boolean = + searchView?.findViewById(androidx.appcompat.R.id.search_edit_frame)?.isVisible == true + /** * Sets up a custom back-press handler for this activity. * @@ -1295,75 +1176,40 @@ class FileDisplayActivity : } private fun handleBackPressImpl(before: () -> Unit = {}, after: () -> Unit = {}) { + val currentLeftFragment = leftFragment + + before() when { - isSearchOpen() -> { - before() - resetSearchAction() - after() - } + isSearchOpen() -> resetSearchAction() - isDrawerOpen -> { - before() - closeDrawer() - after() - } + isDrawerOpen -> closeDrawer() - isShowingDetailsFromPreview && leftFragment is FileDetailFragment -> { - before() + isShowingDetailsFromPreview && currentLeftFragment is FileDetailFragment -> NavigationAnimator(this).finishWithSlideDown() - after() - } // pop back if current fragment is AlbumItemsFragment - isAlbumItemsFragment -> { - before() - popBack() - after() - } + isAlbumItemsFragment -> popBack() - leftFragment is OCFileListFragment -> { - before() - handleOCFileListFragmentBackPress() - after() - } + currentLeftFragment is OCFileListFragment -> handleOCFileListFragmentBackPress(currentLeftFragment) - else -> { - before() - popBack() - after() - } + else -> popBack() } + after() } - private fun handleOCFileListFragmentBackPress() { - val fragment = leftFragment as OCFileListFragment - - when { - // root - isRoot(getCurrentDir()) -> { - if (fragment.shouldNavigateBackToAllFiles()) { - navigateToAllFiles() - } else { - finish() - } - } - - // Normal folder navigation (go up) also works for shared tab - else -> { - browseUp(fragment) - } + private fun handleOCFileListFragmentBackPress(fragment: OCFileListFragment) { + // Normal folder navigation (go up) also works for shared tab + if (!isRoot(getCurrentDir())) { + browseUp(fragment) + return } + + if (fragment.shouldNavigateBackToAllFiles()) navigateToAllFiles() else finish() } override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { android.R.id.home -> { - when { - shouldOpenDrawer() -> openDrawer() - - else -> { - handleBackPressImpl() - } - } + if (shouldOpenDrawer()) openDrawer() else handleBackPressImpl() true } @@ -1387,7 +1233,7 @@ class FileDisplayActivity : file = currentFile currentFile?.let { - listOfFiles.setFabVisible(currentFile.canCreateFileAndFolder()) + listOfFiles.setFabVisible(it.canCreateFileAndFolder()) listOfFiles.registerFabListener() } @@ -1396,28 +1242,28 @@ class FileDisplayActivity : } private fun resetSearchAction() { - val leftFragment = this.leftFragment - if (!isSearchOpen() || searchView == null) { - return - } + val currentLeftFragment = leftFragment + val currentSearchView = searchView ?: return + if (!isSearchOpen()) return - searchView?.clearFocus() - val imm = getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager - imm?.hideSoftInputFromWindow(searchView?.windowToken, 0) - searchView?.setQuery("", false) - searchView?.onActionViewCollapsed() + currentSearchView.run { + clearFocus() + val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager + inputMethodManager?.hideSoftInputFromWindow(windowToken, 0) + setQuery("", false) + onActionViewCollapsed() + } - if (isRoot(getCurrentDir()) && leftFragment is OCFileListFragment) { + if (isRoot(getCurrentDir()) && currentLeftFragment is OCFileListFragment) { // Remove the list to the original state - leftFragment.adapter?.let { adapter -> - val listOfHiddenFiles = adapter.listOfHiddenFiles - leftFragment.performSearch("", listOfHiddenFiles, true) + currentLeftFragment.adapter?.let { adapter -> + currentLeftFragment.performSearch("", adapter.listOfHiddenFiles, true) } hideSearchView(getCurrentDir()) setDrawerIndicatorEnabled(isDrawerIndicatorAvailable) } - if (leftFragment is UnifiedSearchFragment) { + if (currentLeftFragment is UnifiedSearchFragment) { showSortListGroup(false) supportFragmentManager.popBackStack() } @@ -1439,18 +1285,15 @@ class FileDisplayActivity : override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.run { - putParcelable(KEY_WAITING_TO_PREVIEW, mWaitingToPreview) + putParcelable(KEY_WAITING_TO_PREVIEW, waitingToPreview) putParcelable(KEY_SYNC_STATE, syncState) - putParcelable(KEY_WAITING_TO_SEND, mWaitingToSend) - if (searchView != null) { - putBoolean(KEY_IS_SEARCH_OPEN, searchView?.isIconified == false) - } + putParcelable(KEY_WAITING_TO_SEND, waitingToSend) + searchView?.let { putBoolean(KEY_IS_SEARCH_OPEN, !it.isIconified) } putString(KEY_SEARCH_QUERY, searchQuery) putBoolean(KEY_IS_SORT_GROUP_VISIBLE, sortListGroupVisibility()) } } - @Suppress("CyclomaticComplexMethod") override fun onResume() { Log_OC.v(TAG, "onResume() start") @@ -1461,14 +1304,8 @@ class FileDisplayActivity : folderRefreshScheduler.start() - if (ocFileListFragment?.isSearchFragment == true) { - ocFileListFragment?.loadSearchState(ocFileListFragment?.arguments) - } - if (isAlbumsFragment || isAlbumItemsFragment) { - highlightNavigationViewItem(R.id.nav_album) - } else { - highlightNavigationViewItem(menuItemId) - } + restoreSearchStateIfNeeded() + highlightCurrentNavigationItem() if (SettingsActivity.isBackPressed) { Log_OC.d(TAG, "User returned from settings activity, skipping reset content logic") @@ -1478,10 +1315,10 @@ class FileDisplayActivity : isFileDisplayActivityResumed = true // Instead of onPostCreate, starting the loading in onResume for children fragments - val leftFragment = this.leftFragment - if (leftFragment !is OCFileListFragment) { - if (leftFragment is FileFragment) { - super.updateActionBarTitleAndHomeButton(leftFragment.file) + val currentLeftFragment = leftFragment + if (currentLeftFragment !is OCFileListFragment) { + if (currentLeftFragment is FileFragment) { + super.updateActionBarTitleAndHomeButton(currentLeftFragment.file) } return } @@ -1489,33 +1326,10 @@ class FileDisplayActivity : if (isToolbarStyleSearch) { setupHomeSearchToolbarWithSortAndListButtons() } - val ocFileListFragment = leftFragment syncAndUpdateFolder(ignoreETag = true, ignoreFocus = true) + refreshListOfFiles(currentLeftFragment, listFragmentJustCreated) - // Try to get the OCFile from the intent, if one was provided when launching this activity. - // 'file' comes from the FileActivity base class and represents the currently opened file or folder. - // We update it only when a valid file is found in the intent. - val startFile = intent?.let { getFileFromIntent(it) }?.also { - file = it - } - - // refresh list of files - if (searchView != null && !TextUtils.isEmpty(searchQuery)) { - searchView?.setQuery(searchQuery, false) - } else if (!ocFileListFragment.isSearchFragment && startFile == null) { - if (!listFragmentJustCreated) { - ocFileListFragment.listDirectory(MainApp.isOnlyOnDevice()) - } - ocFileListFragment.registerFabListener() - updateActionBarTitleAndHomeButton(currentDir) - } else { - ocFileListFragment.listDirectory(startFile, false) - updateActionBarTitleAndHomeButton(startFile) - } - - // show in-app review dialog to user inAppReviewHelper.showInAppReview(this) - checkNotifications() Log_OC.v(TAG, "onResume() end") @@ -1525,6 +1339,43 @@ class FileDisplayActivity : }, ON_RESUMED_RESET_DELAY) } + private fun restoreSearchStateIfNeeded() { + val fragment = ocFileListFragment ?: return + if (fragment.isSearchFragment) { + fragment.loadSearchState(fragment.arguments) + } + } + + private fun highlightCurrentNavigationItem() { + val itemId = if (isAlbumsFragment || isAlbumItemsFragment) R.id.nav_album else menuItemId + highlightNavigationViewItem(itemId) + } + + private fun refreshListOfFiles(fragment: OCFileListFragment, listFragmentJustCreated: Boolean) { + // Try to get the OCFile from the intent, if one was provided when launching this activity. + // 'file' comes from the FileActivity base class and represents the currently opened file or folder. + // We update it only when a valid file is found in the intent. + val startFile = getFileFromIntent(intent)?.also { file = it } + + val currentSearchView = searchView + when { + currentSearchView != null && !searchQuery.isNullOrEmpty() -> currentSearchView.setQuery(searchQuery, false) + + !fragment.isSearchFragment && startFile == null -> { + if (!listFragmentJustCreated) { + fragment.listDirectory(MainApp.isOnlyOnDevice()) + } + fragment.registerFabListener() + updateActionBarTitleAndHomeButton(currentDir) + } + + else -> { + fragment.listDirectory(startFile, false) + updateActionBarTitleAndHomeButton(startFile) + } + } + } + private fun getFileFromIntent(intent: Intent?): OCFile? = intent.getParcelableArgument(EXTRA_FILE, OCFile::class.java) ?: intent?.getStringExtra(EXTRA_FILE_REMOTE_PATH) @@ -1574,8 +1425,7 @@ class FileDisplayActivity : } override fun onSortingOrderChosen(selection: FileSortOrder?) { - val ocFileListFragment = this.listOfFilesFragment - ocFileListFragment?.fileListLayoutManager?.sortFiles(selection) + listOfFilesFragment?.fileListLayoutManager?.sortFiles(selection) } override fun downloadFile(file: OCFile, packageName: String, activityName: String) { @@ -1595,9 +1445,8 @@ class FileDisplayActivity : val syncFolderRemotePath = intent.getStringExtra(FileSyncAdapter.EXTRA_FOLDER_PATH) val id = intent.getStringExtra(FileSyncAdapter.EXTRA_RESULT) val syncResult = DataHolderUtil.getInstance().retrieve(id) - val sameAccount = - account != null && accountName != null && accountName == account.name && storageManager != null - val fileListFragment: OCFileListFragment? = this@FileDisplayActivity.listOfFilesFragment + val sameAccount = accountName != null && accountName == account?.name && storageManager != null + val fileListFragment = this@FileDisplayActivity.listOfFilesFragment // endregion if (sameAccount) { @@ -1687,25 +1536,14 @@ class FileDisplayActivity : currentDir: OCFile, syncFolderRemotePath: String? ) { - if (currentDir.remotePath != syncFolderRemotePath) { - return - } + if (currentDir.remotePath != syncFolderRemotePath) return - if (ocFileListFragment == null) { - return - } - - ocFileListFragment.listDirectory(currentDir, MainApp.isOnlyOnDevice()) + ocFileListFragment?.listDirectory(currentDir, MainApp.isOnlyOnDevice()) } private fun handleScrollBehaviour(ocFileListFragment: OCFileListFragment?) { - if (ocFileListFragment == null) { - return - } - - if (syncState == EmptyListState.LOADING || ocFileListFragment.isLoading) { - return - } + ocFileListFragment ?: return + if (syncState == EmptyListState.LOADING || ocFileListFragment.isLoading) return if (ocFileListFragment.isEmpty) { lockScrolling() @@ -1737,9 +1575,7 @@ class FileDisplayActivity : } private fun handleNonCredentialSyncErrors(syncResult: Any?) { - if (syncResult !is RemoteOperationResult<*>) { - return - } + if (syncResult !is RemoteOperationResult<*>) return when (syncResult.code) { RemoteOperationResult.ResultCode.SSL_RECOVERABLE_PEER_UNVERIFIED -> showUntrustedCertDialog(syncResult) @@ -1758,19 +1594,14 @@ class FileDisplayActivity : } } - private fun checkForRemoteOperationError(syncResult: Any?): Boolean { - if (syncResult !is RemoteOperationResult<*>) { - return false - } - - return RemoteOperationResult.ResultCode.UNAUTHORIZED == syncResult.code || - (syncResult.isException && syncResult.exception is AuthenticatorException) - } + private fun checkForRemoteOperationError(syncResult: Any?): Boolean = syncResult is RemoteOperationResult<*> && + ( + RemoteOperationResult.ResultCode.UNAUTHORIZED == syncResult.code || + (syncResult.isException && syncResult.exception is AuthenticatorException) + ) private fun onSyncFinished() { - if (syncState != EmptyListState.LOADING) { - return - } + if (syncState != EmptyListState.LOADING) return syncState = when { MainApp.isOnlyOnDevice() -> EmptyListState.ONLY_ON_DEVICE @@ -1790,72 +1621,78 @@ class FileDisplayActivity : private inner class FileUploadCompletedReceiver : BroadcastReceiver() { private val tag = "UploadFinishReceiver" - @Suppress("CyclomaticComplexMethod") override fun onReceive(context: Context?, intent: Intent) { Log_OC.d(tag, "upload finish received broadcast") val uploadedRemotePath = intent.getStringExtra(FileUploadEventBroadcaster.EXTRA_REMOTE_PATH) val accountName = intent.getStringExtra(FileUploadEventBroadcaster.EXTRA_ACCOUNT_NAME) - val account = getAccount() - val sameAccount = accountName != null && account != null && accountName == account.name - val currentDir = getCurrentDir() + val sameAccount = accountName != null && accountName == getAccount()?.name + val currentDirRemotePath = getCurrentDir()?.remotePath val isDescendant = - currentDir != null && uploadedRemotePath != null && uploadedRemotePath.startsWith(currentDir.remotePath) + currentDirRemotePath != null && uploadedRemotePath?.startsWith(currentDirRemotePath) == true if (sameAccount && isDescendant) { updateListOfFilesFragment() } val uploadWasFine = intent.getBooleanExtra(FileUploadEventBroadcaster.EXTRA_UPLOAD_RESULT, false) + val currentFile = file + val oldRemotePath = intent.getStringExtra(FileUploadEventBroadcaster.EXTRA_OLD_REMOTE_PATH) + val renamedInUpload = currentFile != null && currentFile.remotePath == oldRemotePath + val sameFile = currentFile != null && (currentFile.remotePath == uploadedRemotePath || renamedInUpload) - var renamedInUpload = false - var sameFile = false - if (file != null) { - renamedInUpload = - file?.remotePath == intent.getStringExtra(FileUploadEventBroadcaster.EXTRA_OLD_REMOTE_PATH) - sameFile = file?.remotePath == uploadedRemotePath || renamedInUpload + val fileDetailFragment = leftFragment as? FileDetailFragment + if (sameAccount && sameFile && fileDetailFragment != null) { + updateFileDetailsAfterUpload(fileDetailFragment, uploadedRemotePath, uploadWasFine, renamedInUpload) } - if (sameAccount && sameFile && this@FileDisplayActivity.leftFragment is FileDetailFragment) { - val fileDetailFragment = leftFragment as FileDetailFragment - if (uploadWasFine) { - file = storageManager.getFileByPath(uploadedRemotePath) - } else { - // TODO remove upload progress bar after upload failed. - Log_OC.d(TAG, "Remove upload progress bar after upload failed") - } - if (renamedInUpload && !uploadedRemotePath.isNullOrBlank()) { - val newName = File(uploadedRemotePath).name - SnackbarUtil.show( - this@FileDisplayActivity, - R.string.filedetails_renamed_in_upload_msg, - newName - ) - } + refreshAlbumScreensAfterUpload() + } + + private fun updateFileDetailsAfterUpload( + fileDetailFragment: FileDetailFragment, + uploadedRemotePath: String?, + uploadWasFine: Boolean, + renamedInUpload: Boolean + ) { + if (uploadWasFine) { + file = storageManager.getFileByPath(uploadedRemotePath) + } else { + // TODO remove upload progress bar after upload failed. + Log_OC.d(TAG, "Remove upload progress bar after upload failed") + } + + if (renamedInUpload && !uploadedRemotePath.isNullOrBlank()) { + val newName = File(uploadedRemotePath).name + SnackbarUtil.show(this@FileDisplayActivity, R.string.filedetails_renamed_in_upload_msg, newName) + } + + if (uploadWasFine || file?.fileExists() == true) { + fileDetailFragment.updateFileDetails(false, true) + } else { + onBackPressedDispatcher.onBackPressed() + } - if (uploadWasFine || file?.fileExists() == true) { - fileDetailFragment.updateFileDetails(false, true) - } else { - onBackPressedDispatcher.onBackPressed() - } + if (uploadWasFine) { + file?.let { forcePreviewIfImageOrText(it) } + } + } - // Force the preview if the file is an image or text file - if (uploadWasFine) { - file?.let { - if (PreviewMediaFragment.canBePreviewed(it)) { - startImagePreview(it, true) - } else if (PreviewTextFileFragment.canBePreviewed(it)) { - startTextPreview(it, true) - } - } - } + private fun forcePreviewIfImageOrText(uploadedFile: OCFile) { + when { + PreviewMediaFragment.canBePreviewed(uploadedFile) -> startImagePreview(uploadedFile, true) + PreviewTextFileFragment.canBePreviewed(uploadedFile) -> startTextPreview(uploadedFile, true) } + } + + private fun refreshAlbumScreensAfterUpload() { + when { + isAlbumsFragment -> + (supportFragmentManager.findFragmentByTag(AlbumsFragment.TAG) as? AlbumsFragment)?.refreshAlbums() - // notify when upload is finished and user is on albums screen - if (isAlbumsFragment) { - (supportFragmentManager.findFragmentByTag(AlbumsFragment.TAG) as AlbumsFragment).refreshAlbums() - } else if (isAlbumItemsFragment) { - (supportFragmentManager.findFragmentByTag(AlbumItemsFragment.TAG) as AlbumItemsFragment).refreshData() + isAlbumItemsFragment -> + (supportFragmentManager.findFragmentByTag(AlbumItemsFragment.TAG) as? AlbumItemsFragment) + ?.refreshData() } } } @@ -1881,8 +1718,7 @@ class FileDisplayActivity : // region FileDownloadWorker receivers private fun getFileFromFileDownloadWorker(intent: Intent): OCFile? { val remotePath = intent.getStringExtra(FileDownloadEventBroadcaster.EXTRA_REMOTE_PATH) - val file = fileDataStorageManager.getFileByDecryptedRemotePath(remotePath) ?: return null - return file + return fileDataStorageManager.getFileByDecryptedRemotePath(remotePath) } private inner class FileDownloadStartedReceiver : BroadcastReceiver() { @@ -1902,7 +1738,7 @@ class FileDisplayActivity : updateFileDownloadIndicator(FileDownloadIndicator.Downloaded, it) } - if (fileIDForImmediatePreview == -1L) { + if (fileIDForImmediatePreview == NO_IMMEDIATE_PREVIEW_FILE_ID) { Log_OC.d(TAG, "updating ui for file download") updateUIForFileDownload(intent) return @@ -1912,11 +1748,9 @@ class FileDisplayActivity : val downloadedRemotePath = intent.getStringExtra(FileDownloadEventBroadcaster.EXTRA_REMOTE_PATH) val currentFile = storageManager.getFileByDecryptedRemotePath(downloadedRemotePath) ?: return - if (fileIDForImmediatePreview != currentFile.fileId || !currentFile.isDown) { - return - } + if (fileIDForImmediatePreview != currentFile.fileId || !currentFile.isDown) return - fileIDForImmediatePreview = -1 + fileIDForImmediatePreview = NO_IMMEDIATE_PREVIEW_FILE_ID if (PreviewMediaFragment.canBePreviewed(currentFile)) { startImagePreview(currentFile, currentFile.isDown) } else { @@ -1924,14 +1758,11 @@ class FileDisplayActivity : } } - @Suppress("CyclomaticComplexMethod") private fun updateUIForFileDownload(intent: Intent) { - val sameAccount = isSameAccount(intent) val downloadedRemotePath = intent.getStringExtra(FileDownloadEventBroadcaster.EXTRA_REMOTE_PATH) val downloadBehaviour = intent.getStringExtra(FileDownloadEventBroadcaster.EXTRA_DOWNLOAD_BEHAVIOUR) - val isDescendant = isDescendant(downloadedRemotePath) - if (sameAccount && isDescendant) { + if (isSameAccount(intent) && isDescendant(downloadedRemotePath)) { updateListOfFilesFragment() val intentAction = intent.action if (intentAction != null && downloadedRemotePath != null) { @@ -1943,42 +1774,36 @@ class FileDisplayActivity : } } - if (mWaitingToSend != null) { + waitingToSend?.let { previousWaitingToSend -> // update file after downloading - mWaitingToSend = storageManager.getFileByRemoteId(mWaitingToSend?.remoteId) - if (mWaitingToSend != null && - mWaitingToSend?.isDown == true && - OCFileListFragment.DOWNLOAD_SEND == downloadBehaviour - ) { + val updatedWaitingToSend = storageManager.getFileByRemoteId(previousWaitingToSend.remoteId) + waitingToSend = updatedWaitingToSend + if (updatedWaitingToSend?.isDown == true && OCFileListFragment.DOWNLOAD_SEND == downloadBehaviour) { val packageName = intent.getStringExtra(FileDownloadEventBroadcaster.EXTRA_PACKAGE_NAME) ?: return val activityName = intent.getStringExtra(FileDownloadEventBroadcaster.EXTRA_ACTIVITY_NAME) ?: return sendDownloadedFile(packageName, activityName) } } - if (mWaitingToPreview != null) { - mWaitingToPreview = storageManager.getFileByRemoteId(mWaitingToPreview?.remoteId) - if (mWaitingToPreview != null && - mWaitingToPreview?.isDown == true && + waitingToPreview?.let { previousWaitingToPreview -> + val updatedWaitingToPreview = storageManager.getFileByRemoteId(previousWaitingToPreview.remoteId) + waitingToPreview = updatedWaitingToPreview + if (updatedWaitingToPreview?.isDown == true && EditImageActivity.OPEN_IMAGE_EDITOR == downloadBehaviour ) { - mWaitingToPreview?.let { - startImageEditor(it) - } + startImageEditor(updatedWaitingToPreview) } } } fun isDescendant(downloadedRemotePath: String?): Boolean { - val currentDir = getCurrentDir() - return currentDir != null && - downloadedRemotePath != null && - downloadedRemotePath.startsWith(currentDir.remotePath) + val currentDirRemotePath = getCurrentDir()?.remotePath ?: return false + return downloadedRemotePath?.startsWith(currentDirRemotePath) == true } fun isSameAccount(intent: Intent): Boolean { val accountName = intent.getStringExtra(FileDownloadEventBroadcaster.EXTRA_ACCOUNT_NAME) - return accountName != null && account != null && accountName == account.name + return accountName != null && accountName == account?.name } } // endregion @@ -2021,11 +1846,11 @@ class FileDisplayActivity : * @param activeTab the active tab in the details view */ override fun showDetails(file: OCFile?, activeTab: Int) { - val currentUser = user.orElseThrow(Supplier { RuntimeException() }) + val currentUser = user.orElse(null) ?: return resetScrolling(true) - val detailFragment: Fragment = FileDetailFragment.newInstance(file, currentUser, activeTab) + val detailFragment = FileDetailFragment.newInstance(file, currentUser, activeTab) setLeftFragment(detailFragment, false) // swap now, so no frame shows the list without its sort row supportFragmentManager.executePendingTransactions() @@ -2038,9 +1863,9 @@ class FileDisplayActivity : @VisibleForTesting fun lockScrolling() { binding.appbar.appbar.setExpanded(true, false) - val appbarParams = binding.appbar.toolbarFrame.layoutParams as AppBarLayout.LayoutParams - appbarParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_NO_SCROLL) - binding.appbar.toolbarFrame.layoutParams = appbarParams + binding.appbar.toolbarFrame.updateLayoutParams { + scrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_NO_SCROLL + } } /** @@ -2048,22 +1873,18 @@ class FileDisplayActivity : */ @VisibleForTesting fun resetScrolling(expandAppBar: Boolean) { - val appbarParams = binding.appbar.toolbarFrame.layoutParams as AppBarLayout.LayoutParams - appbarParams.setScrollFlags( - AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL or AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS - ) - binding.appbar.toolbarFrame.layoutParams = appbarParams + binding.appbar.toolbarFrame.updateLayoutParams { + scrollFlags = + AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL or AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS + } if (expandAppBar) { binding.appbar.appbar.setExpanded(true, false) } } public override fun updateActionBarTitleAndHomeButton(chosenFile: OCFile?) { - var chosenFile = chosenFile - if (chosenFile == null) { - chosenFile = file // if no file is passed, current file decides - } - super.updateActionBarTitleAndHomeButton(chosenFile) + // if no file is passed, current file decides + super.updateActionBarTitleAndHomeButton(chosenFile ?: file) supportInvalidateOptionsMenu() } @@ -2121,33 +1942,38 @@ class FileDisplayActivity : return } - if (MimeTypeUtil.isVCard(file)) { - startContactListFragment(file) - } else if (MimeTypeUtil.isPDF(file)) { - startPdfPreview(file) - } else if (PreviewTextFileFragment.canBePreviewed(file)) { - setFabVisible?.onComplete(false) - startTextPreview(file, false) - } else if (canPreviewInAudioPlayer(file)) { - setFabVisible?.onComplete(false) - startAudioPreview(file, true, false) - } else { - fileOperationsHelper.openFile(file) + when { + MimeTypeUtil.isVCard(file) -> startContactListFragment(file) + + MimeTypeUtil.isPDF(file) -> startPdfPreview(file) + + PreviewTextFileFragment.canBePreviewed(file) -> { + setFabVisible?.onComplete(false) + startTextPreview(file, false) + } + + canPreviewInAudioPlayer(file) -> { + setFabVisible?.onComplete(false) + startAudioPreview(file, true, false) + } + + else -> fileOperationsHelper.openFile(file) } } fun refreshCurrentDirectory() { val currentDir = getCurrentDir()?.let { storageManager.getFileByDecryptedRemotePath(it.remotePath) } - val fileListFragment = lastFragment() as? OCFileListFragment ?: listOfFilesFragment fileListFragment?.listDirectory(currentDir, MainApp.isOnlyOnDevice()) } private fun handleDownloadWorkerState() { - if (mWaitingToPreview != null && storageManager != null) { - mWaitingToPreview = mWaitingToPreview?.fileId?.let { storageManager.getFileById(it) } - if (mWaitingToPreview != null && mWaitingToPreview?.isDown == false) { - requestForDownload() - } + val previousWaitingToPreview = waitingToPreview ?: return + if (storageManager == null) return + + val updatedWaitingToPreview = storageManager.getFileById(previousWaitingToPreview.fileId) + waitingToPreview = updatedWaitingToPreview + if (updatedWaitingToPreview?.isDown == false) { + requestForDownload() } } @@ -2178,88 +2004,59 @@ class FileDisplayActivity : super.onRemoteOperationFinish(operation, result) when (operation) { - is RemoveFileOperation -> { - onRemoveFileOperationFinish(operation, result) - } + is RemoveFileOperation -> onRemoveFileOperationFinish(operation, result) - is RenameFileOperation -> { - onRenameFileOperationFinish(operation, result) - } + is RenameFileOperation -> onRenameFileOperationFinish(operation, result) - is SynchronizeFileOperation -> { - onSynchronizeFileOperationFinish(operation, result) - } + is SynchronizeFileOperation -> onSynchronizeFileOperationFinish(operation, result) - is CreateFolderOperation -> { - onCreateFolderOperationFinish(operation, result) - } + is CreateFolderOperation -> onCreateFolderOperationFinish(operation, result) - is MoveFileOperation -> { - onMoveFileOperationFinish(operation, result) - } + is MoveFileOperation -> onMoveFileOperationFinish(operation, result) - is CopyFileOperation -> { - onCopyFileOperationFinish(operation, result) - } + is CopyFileOperation -> onCopyFileOperationFinish(operation, result) - is RestoreFileVersionRemoteOperation -> { - onRestoreFileVersionOperationFinish(result) - } + is RestoreFileVersionRemoteOperation -> onRestoreFileVersionOperationFinish(result) - is CreateNewAlbumRemoteOperation -> { - albumOperationListener.onCreateAlbumOperationFinish(operation, result) - } + is CreateNewAlbumRemoteOperation -> albumOperationListener.onCreateAlbumOperationFinish(operation, result) - is CopyFileToAlbumOperation -> { - albumOperationListener.onCopyAlbumFileOperationFinish(operation, result) - } + is CopyFileToAlbumOperation -> albumOperationListener.onCopyAlbumFileOperationFinish(operation, result) - is RenameAlbumRemoteOperation -> { - albumOperationListener.onRenameAlbumOperationFinish(operation, result) - } + is RenameAlbumRemoteOperation -> albumOperationListener.onRenameAlbumOperationFinish(operation, result) - is RemoveAlbumRemoteOperation -> { - albumOperationListener.onRemoveAlbumOperationFinish(operation, result) - } + is RemoveAlbumRemoteOperation -> albumOperationListener.onRemoveAlbumOperationFinish(operation, result) - is PublicShareLinkAlbumRemoteOperation -> { + is PublicShareLinkAlbumRemoteOperation -> albumOperationListener.onAlbumPublicLinkOperationFinish(operation, result) - } } } private val fileListFragment: OCFileListFragment? - get() = if (lastFragment() is OCFileListFragment) lastFragment() as OCFileListFragment else listOfFilesFragment + get() = lastFragment() as? OCFileListFragment ?: listOfFilesFragment private fun refreshGalleryFragmentIfNeeded() { - val fileListFragment = this.fileListFragment if (fileListFragment is GalleryFragment) { startPhotoSearch(R.id.nav_gallery) } } private fun refreshShowDetails() { - val details = this.leftFragment - if (details is FileFragment) { - var file = details.file - if (file != null) { - file = storageManager.getFileByPath(file.remotePath) - if (details is PreviewTextFragment) { - // Refresh OCFile of the fragment - (details as PreviewTextFileFragment).updateFile(file) - } else { - showDetails(file) - } + val details = leftFragment as? FileFragment ?: return + + details.file?.let { + val refreshedFile = storageManager.getFileByPath(it.remotePath) + if (details is PreviewTextFragment) { + // Refresh OCFile of the fragment + (details as? PreviewTextFileFragment)?.updateFile(refreshedFile) + } else { + showDetails(refreshedFile) } - supportInvalidateOptionsMenu() } + supportInvalidateOptionsMenu() } val deleteBatchTracker = DeleteBatchTracker(onAllDeletesFinished = { - if (leftFragment is GalleryFragment) { - val galleryFragment = leftFragment as GalleryFragment - galleryFragment.onRefresh() - } + (leftFragment as? GalleryFragment)?.onRefresh() }) /** @@ -2271,8 +2068,7 @@ class FileDisplayActivity : private fun onRemoveFileOperationFinish(operation: RemoveFileOperation, result: RemoteOperationResult<*>) { deleteBatchTracker.onSingleDeleteFinished() if (!result.isSuccess && result.isSslRecoverableException) { - mLastSslUntrustedServerResult = result - showUntrustedCertDialog(mLastSslUntrustedServerResult) + showUntrustedCertDialogFor(result) return } @@ -2283,30 +2079,42 @@ class FileDisplayActivity : val removedFile = operation.file file?.let { playbackModel.stopPlaying(it) } - val leftFragment = this.leftFragment + val currentLeftFragment = leftFragment // check if file is still available, if so do nothing val fileAvailable = storageManager.fileExists(removedFile.fileId) - if (leftFragment is FileFragment && !fileAvailable && removedFile == leftFragment.file) { + if (currentLeftFragment is FileFragment && !fileAvailable && removedFile == currentLeftFragment.file) { file = storageManager.getFileById(removedFile.parentId) resetScrollingAndUpdateActionBar() } - if (leftFragment is OCFileListFragment && operation.onlyLocalCopy) { - leftFragment.adapter?.notifyItemChanged(removedFile) - } else if (leftFragment is OCFileListFragment) { - leftFragment.adapter?.removeFile(removedFile) - - if (leftFragment.adapter?.isEmpty == true) { - val emptyState = leftFragment.searchEvent?.toSearchType() ?: SearchType.NO_SEARCH - leftFragment.setEmptyListMessage(emptyState) - } + if (currentLeftFragment is OCFileListFragment) { + updateFileListAfterRemoval(currentLeftFragment, removedFile, operation.onlyLocalCopy) } supportInvalidateOptionsMenu() fetchRecommendedFilesIfNeeded(ignoreETag = true, currentDir) } + private fun updateFileListAfterRemoval(fragment: OCFileListFragment, removedFile: OCFile, onlyLocalCopy: Boolean) { + val adapter = fragment.adapter ?: return + if (onlyLocalCopy) { + adapter.notifyItemChanged(removedFile) + return + } + + adapter.removeFile(removedFile) + if (adapter.isEmpty) { + val emptyState = fragment.searchEvent?.toSearchType() ?: SearchType.NO_SEARCH + fragment.setEmptyListMessage(emptyState) + } + } + + private fun showUntrustedCertDialogFor(result: RemoteOperationResult<*>) { + mLastSslUntrustedServerResult = result + showUntrustedCertDialog(result) + } + override fun onAutoUploadFolderRemoved( entities: List, filesToRemove: List, @@ -2319,38 +2127,12 @@ class FileDisplayActivity : titleIconId = R.drawable.ic_info, positiveButtonTextId = R.string.common_delete, negativeButtonTextId = R.string.common_cancel, - neutralButtonTextId = -1 + neutralButtonTextId = NO_NEUTRAL_BUTTON ) dialog.setOnConfirmationListener(object : ConfirmationDialogFragment.ConfirmationDialogFragmentListener { override fun onConfirmation(callerTag: String?) { - lifecycleScope.launch(Dispatchers.IO) { - entities.forEach { entity -> - entity.id?.toLong()?.let { - fileUploadHelper.removeEntityFromUploadEntities(it) - syncedFolderProvider.deleteSyncedFolder(it) - } - } - - connectivityService.isNetworkAndServerAvailable { isAvailable -> - if (isAvailable) { - fileOperationsHelper?.removeFiles( - filesToRemove, - onlyLocalCopy, - true - ) - } else { - if (onlyLocalCopy) { - fileOperationsHelper?.removeFiles(filesToRemove, true, true) - } else { - filesToRemove.forEach { file -> - fileDataStorageManager.addRemoveFileOfflineOperation(file) - } - } - } - onFilesRemoved() - } - } + removeAutoUploadFolders(entities, filesToRemove, onlyLocalCopy) } override fun onNeutral(callerTag: String?) = Unit @@ -2362,30 +2144,56 @@ class FileDisplayActivity : } } + private fun removeAutoUploadFolders( + entities: List, + filesToRemove: List, + onlyLocalCopy: Boolean + ) { + lifecycleScope.launch(Dispatchers.IO) { + entities.mapNotNull { it.id?.toLong() }.forEach { + fileUploadHelper.removeEntityFromUploadEntities(it) + syncedFolderProvider.deleteSyncedFolder(it) + } + + connectivityService.isNetworkAndServerAvailable { isAvailable -> + when { + isAvailable -> fileOperationsHelper?.removeFiles(filesToRemove, onlyLocalCopy, true) + onlyLocalCopy -> fileOperationsHelper?.removeFiles(filesToRemove, true, true) + else -> filesToRemove.forEach { fileDataStorageManager.addRemoveFileOfflineOperation(it) } + } + onFilesRemoved() + } + } + } + private fun onRestoreFileVersionOperationFinish(result: RemoteOperationResult<*>) { - if (result.isSuccess) { - val file = getFile() + if (!result.isSuccess) { + SnackbarUtil.show(this, R.string.file_version_restored_error) + return + } - // delete old local copy - if (file?.isDown == true) { - val list: MutableList = ArrayList() - list.add(file) - fileOperationsHelper.removeFiles(list, true, true) + val restoredFile = getFile() - // download new version, only if file was previously download - showSyncLoadingDialog(file.isFolder) - fileOperationsHelper.syncFileOrFolder(file) - } + // delete old local copy + if (restoredFile?.isDown == true) { + fileOperationsHelper.removeFiles(listOf(restoredFile), true, true) - val parent = file?.let { storageManager.getFileById(it.parentId) } - startSyncFolderOperation(parent, ignoreETag = true, ignoreFocus = true) + // download new version, only if file was previously download + showSyncLoadingDialog(restoredFile.isFolder) + fileOperationsHelper.syncFileOrFolder(restoredFile) + } - val leftFragment = this.leftFragment - if (leftFragment is FileDetailFragment) { - leftFragment.fileDetailActivitiesFragment?.reload() - } - } else { - SnackbarUtil.show(this, R.string.file_version_restored_error) + val parent = restoredFile?.let { storageManager.getFileById(it.parentId) } + startSyncFolderOperation(parent, ignoreETag = true, ignoreFocus = true) + + (leftFragment as? FileDetailFragment)?.fileDetailActivitiesFragment?.reload() + } + + private fun showErrorCauseMessage(result: RemoteOperationResult<*>, operation: RemoteOperation<*>?) { + try { + SnackbarUtil.show(this, ErrorMessageAdapter.getErrorCauseMessage(result, operation, resources)) + } catch (e: Resources.NotFoundException) { + Log_OC.e(TAG, "Error while trying to show fail message ", e) } } @@ -2397,14 +2205,7 @@ class FileDisplayActivity : */ private fun onMoveFileOperationFinish(operation: MoveFileOperation?, result: RemoteOperationResult<*>) { if (!result.isSuccess) { - try { - SnackbarUtil.show( - this, - ErrorMessageAdapter.getErrorCauseMessage(result, operation, getResources()) - ) - } catch (e: Resources.NotFoundException) { - Log_OC.e(TAG, "Error while trying to show fail message ", e) - } + showErrorCauseMessage(result, operation) } } @@ -2415,19 +2216,13 @@ class FileDisplayActivity : * @param result Result of the copy operation. */ private fun onCopyFileOperationFinish(operation: CopyFileOperation?, result: RemoteOperationResult<*>) { - if (result.isSuccess) { - updateListOfFilesFragment() - refreshGalleryFragmentIfNeeded() - } else { - try { - SnackbarUtil.show( - this, - ErrorMessageAdapter.getErrorCauseMessage(result, operation, getResources()) - ) - } catch (e: Resources.NotFoundException) { - Log_OC.e(TAG, "Error while trying to show fail message ", e) - } + if (!result.isSuccess) { + showErrorCauseMessage(result, operation) + return } + + updateListOfFilesFragment() + refreshGalleryFragmentIfNeeded() } /** @@ -2437,29 +2232,23 @@ class FileDisplayActivity : * @param result Result of the renaming. */ private fun onRenameFileOperationFinish(operation: RenameFileOperation, result: RemoteOperationResult<*>) { - val optionalUser = user + val currentUser = user.orElse(null) val renamedFile = operation.file - if (!result.isSuccess || optionalUser.isEmpty) { - SnackbarUtil.show( - this, - ErrorMessageAdapter.getErrorCauseMessage(result, operation, getResources()) - ) + if (!result.isSuccess || currentUser == null) { + SnackbarUtil.show(this, ErrorMessageAdapter.getErrorCauseMessage(result, operation, resources)) if (result.isSslRecoverableException) { - mLastSslUntrustedServerResult = result - showUntrustedCertDialog(mLastSslUntrustedServerResult) + showUntrustedCertDialogFor(result) } return } - val currentUser = optionalUser.get() - val leftFragment = this.leftFragment - if (leftFragment is FileFragment) { - onRenameFileOperationFinishForFileFragment(leftFragment, renamedFile, currentUser) + (leftFragment as? FileFragment)?.let { + onRenameFileOperationFinishForFileFragment(it, renamedFile, currentUser) } - val file = storageManager.getFileById(renamedFile.parentId) - val isCurrentDirParentDirOfGivenFile = (file != null && file == currentDir) + val parentDir = storageManager.getFileById(renamedFile.parentId) + val isCurrentDirParentDirOfGivenFile = parentDir != null && parentDir == currentDir // checking current dir against renamed file's parent will always fail when user is in Shared/Favorites root. // thus check is current dir is root or not as well @@ -2503,12 +2292,12 @@ class FileDisplayActivity : operation: SynchronizeFileOperation, result: RemoteOperationResult<*> ) { - if (result.isSuccess && operation.transferWasRequested) { - val syncedFile = operation.localFile ?: return - onTransferStateChanged(syncedFile, true, true) - supportInvalidateOptionsMenu() - refreshShowDetails() - } + if (!result.isSuccess || !operation.transferWasRequested) return + + val syncedFile = operation.localFile ?: return + onTransferStateChanged(syncedFile, true, true) + supportInvalidateOptionsMenu() + refreshShowDetails() } /** @@ -2518,38 +2307,30 @@ class FileDisplayActivity : * @param result Result of the creation. */ private fun onCreateFolderOperationFinish(operation: CreateFolderOperation, result: RemoteOperationResult<*>) { - if (result.isSuccess) { - val fileListFragment = this.listOfFilesFragment - if (operation.shouldEncrypt()) { - val file = storageManager.getFileByDecryptedRemotePath(operation.remotePath) - if (file == null) { - Log_OC.e( - TAG, - "onCreateFolderOperationFinish(): file not saved after create folder operation, cannot encrypt" - ) - return - } - fileOperationsHelper.toggleEncryption(file, true) - return + if (!result.isSuccess) { + if (RemoteOperationResult.ResultCode.FOLDER_ALREADY_EXISTS == result.code) { + SnackbarUtil.show(this, R.string.folder_already_exists) + } else { + showErrorCauseMessage(result, operation) } + return + } - storageManager.getFileByDecryptedRemotePath(operation.getRemotePath())?.let { - fileListFragment?.onItemClicked(it) - } - } else { - try { - if (RemoteOperationResult.ResultCode.FOLDER_ALREADY_EXISTS == result.code) { - SnackbarUtil.show(this, R.string.folder_already_exists) - } else { - SnackbarUtil.show( - this, - ErrorMessageAdapter.getErrorCauseMessage(result, operation, getResources()) - ) - } - } catch (e: Resources.NotFoundException) { - Log_OC.e(TAG, "Error while trying to show fail message ", e) - } + val fileListFragment = listOfFilesFragment + val createdFolder = storageManager.getFileByDecryptedRemotePath(operation.remotePath) + if (!operation.shouldEncrypt()) { + createdFolder?.let { fileListFragment?.onItemClicked(it) } + return + } + + if (createdFolder == null) { + Log_OC.e( + TAG, + "onCreateFolderOperationFinish(): file not saved after create folder operation, cannot encrypt" + ) + return } + fileOperationsHelper.toggleEncryption(createdFolder, true) } /** @@ -2557,26 +2338,21 @@ class FileDisplayActivity : */ override fun onTransferStateChanged(file: OCFile, downloading: Boolean, uploading: Boolean) { updateListOfFilesFragment() - val leftFragment = this.leftFragment - val optionalUser = user - if (leftFragment is FileDetailFragment && file == leftFragment.file && optionalUser.isPresent) { - val currentUser = optionalUser.get() - if (downloading || uploading) { - leftFragment.updateFileDetails(file, currentUser) - } else { - if (!file.fileExists()) { - resetScrollingAndUpdateActionBar() - } else { - leftFragment.updateFileDetails(false, true) - } - } + val fileDetailFragment = leftFragment as? FileDetailFragment ?: return + if (file != fileDetailFragment.file) return + val currentUser = user.orElse(null) ?: return + + when { + downloading || uploading -> fileDetailFragment.updateFileDetails(file, currentUser) + !file.fileExists() -> resetScrollingAndUpdateActionBar() + else -> fileDetailFragment.updateFileDetails(false, true) } } private fun requestForDownload() { - val user = user.orElseThrow(Supplier { RuntimeException() }) - mWaitingToPreview?.let { - FileDownloadHelper.instance().downloadFileIfNotStartedBefore(user, it) + val currentUser = user.orElse(null) ?: return + waitingToPreview?.let { + FileDownloadHelper.instance().downloadFileIfNotStartedBefore(currentUser, it) } } @@ -2603,7 +2379,7 @@ class FileDisplayActivity : fun startSyncFolderOperation(folder: OCFile?, ignoreETag: Boolean, ignoreFocus: Boolean = false) { Log_OC.d(TAG, "startSyncFolderOperation called, ignoreEtag: $ignoreETag, ignoreFocus: $ignoreFocus") - if (!searchQuery.isNullOrEmpty() || !user.isPresent) { + if (!searchQuery.isNullOrEmpty() || user.isEmpty) { return } @@ -2634,82 +2410,73 @@ class FileDisplayActivity : } private fun executeSyncFolderOperation(folder: OCFile?, ignoreETag: Boolean) { - val folder = folder ?: return - - user.ifPresent { user -> - syncState = EmptyListState.LOADING - - RefreshFolderOperation( - folder, - System.currentTimeMillis(), - false, - ignoreETag, - storageManager, - user, - applicationContext - ).execute( - account, - this, - { _, _ -> onSyncFinished() }, - handler, - null - ) + folder ?: return + val currentUser = user.orElse(null) ?: return + + syncState = EmptyListState.LOADING + + RefreshFolderOperation( + folder, + System.currentTimeMillis(), + false, + ignoreETag, + storageManager, + currentUser, + applicationContext + ).execute( + account, + this, + { _, _ -> onSyncFinished() }, + handler, + null + ) - fetchRecommendedFilesIfNeeded(ignoreETag, folder) - } + fetchRecommendedFilesIfNeeded(ignoreETag, folder) } private fun fetchRecommendedFilesIfNeeded(ignoreETag: Boolean, folder: OCFile?) { - val optionalCapabilities = capabilities - if (optionalCapabilities.isEmpty) { - return - } - - if (folder?.isRootDirectory == false || optionalCapabilities.get().recommendations.isFalse) { - return - } + val capability = capabilities.orElse(null) ?: return + if (folder?.isRootDirectory == false || capability.recommendations.isFalse) return - user.ifPresent { user -> - val accountName = user.accountName - val fragment = this.listOfFilesFragment - lifecycleScope.launch(Dispatchers.IO) { - val recommendedFiles = filesRepository.fetchRecommendedFiles(accountName, ignoreETag, storageManager) - withContext(Dispatchers.Main) { - fragment?.adapter?.updateRecommendedFiles(recommendedFiles) - } + val accountName = user.orElse(null)?.accountName ?: return + val fragment = listOfFilesFragment + lifecycleScope.launch { + val recommendedFiles = withContext(Dispatchers.IO) { + filesRepository.fetchRecommendedFiles(accountName, ignoreETag, storageManager) } + fragment?.adapter?.updateRecommendedFiles(recommendedFiles) } } private fun requestForDownload(file: OCFile, downloadBehaviour: String, packageName: String, activityName: String) { - val currentUser = user.orElseThrow(Supplier { RuntimeException() }) - if (!FileDownloadHelper.instance().isDownloading(currentUser, file)) { - FileDownloadHelper.instance().downloadFile( - currentUser, - file, - downloadBehaviour, - DownloadType.DOWNLOAD, - activityName, - packageName, - null - ) - } + val currentUser = user.orElse(null) ?: return + val downloadHelper = FileDownloadHelper.instance() + if (downloadHelper.isDownloading(currentUser, file)) return + + downloadHelper.downloadFile( + currentUser, + file, + downloadBehaviour, + DownloadType.DOWNLOAD, + activityName, + packageName, + null + ) } private fun sendDownloadedFile(packageName: String, activityName: String) { - val waitingToSend = mWaitingToSend - if (waitingToSend != null) { - val sendIntent = IntentUtil.createSendIntent(this, waitingToSend) - sendIntent.component = ComponentName(packageName, activityName) - - // Show dialog - val sendTitle = getString(R.string.activity_chooser_send_file_title) - startActivity(Intent.createChooser(sendIntent, sendTitle)) - } else { + val fileToSend = waitingToSend ?: run { Log_OC.e(TAG, "Trying to send a NULL OCFile") + return + } + + val sendIntent = IntentUtil.createSendIntent(this, fileToSend).apply { + component = ComponentName(packageName, activityName) } + val sendTitle = getString(R.string.activity_chooser_send_file_title) + startActivity(Intent.createChooser(sendIntent, sendTitle)) - mWaitingToSend = null + waitingToSend = null } /** @@ -2721,10 +2488,8 @@ class FileDisplayActivity : * @param activityName */ fun startDownloadForSending(file: OCFile?, downloadBehaviour: String, packageName: String, activityName: String) { - mWaitingToSend = file - mWaitingToSend?.let { - requestForDownload(it, downloadBehaviour, packageName, activityName) - } + waitingToSend = file + file?.let { requestForDownload(it, downloadBehaviour, packageName, activityName) } } fun startImagePreview( @@ -2739,18 +2504,22 @@ class FileDisplayActivity : if (showPreview) { NavigationAnimator(this).scaleUp(intent, sourceView) } else { - val helper = FileOperationsHelper( - this, - userAccountManager, - connectivityService, - editorUtils - ) - helper.startSyncForFileAndIntent(file, intent) + startSyncForFileAndIntent(file, intent) } } + private fun startSyncForFileAndIntent(file: OCFile?, intent: Intent) { + FileOperationsHelper(this, userAccountManager, connectivityService, editorUtils) + .startSyncForFileAndIntent(file, intent) + } + + private fun previewIntentFor(file: OCFile?, previewKey: String): Intent = Intent().apply { + putExtra(EXTRA_FILE, file) + putExtra(previewKey, true) + } + private fun imagePreviewIntent(file: OCFile, type: VirtualFolderType?, mediaState: MediaState?): Intent? { - if (user.isEmpty) { + val currentUser = user.orElse(null) ?: run { Log_OC.e(TAG, "cannot start image preview") return null } @@ -2758,7 +2527,7 @@ class FileDisplayActivity : return Intent(this, PreviewMediaActivity::class.java).apply { putExtra(EXTRA_FILE, file) putExtra(EXTRA_LIVE_PHOTO_FILE, file.livePhotoVideo) - putExtra(EXTRA_USER, user.get()) + putExtra(EXTRA_USER, currentUser) type?.let { putExtra(PreviewMediaActivity.EXTRA_VIRTUAL_TYPE, it) } mediaState?.let { putExtra(PreviewMediaActivity.EXTRA_MEDIA_STATE, it) } } @@ -2768,19 +2537,13 @@ class FileDisplayActivity : * Starts the preview of an audio [OCFile], synchronizing it first when it is not available yet. */ fun startAudioPreview(file: OCFile, showPreview: Boolean, streamMedia: Boolean) { - val user = getUser() - if (!user.isPresent) { - return // not reachable under normal conditions - } + // not reachable under normal conditions + if (user.isEmpty) return + if ((showPreview && file.isDown && !file.isDownloading) || streamMedia) { startAudioPlayer(file) } else { - val previewIntent = Intent() - previewIntent.putExtra(EXTRA_FILE, file) - previewIntent.putExtra(AUDIO_PREVIEW, true) - val fileOperationsHelper = - FileOperationsHelper(this, userAccountManager, connectivityService, editorUtils) - fileOperationsHelper.startSyncForFileAndIntent(file, previewIntent) + startSyncForFileAndIntent(file, previewIntentFor(file, AUDIO_PREVIEW)) } } @@ -2803,24 +2566,16 @@ class FileDisplayActivity : * @param file Text [OCFile] to preview. */ fun startTextPreview(file: OCFile?, showPreview: Boolean) { - val optUser = user - if (!optUser.isPresent) { - // remnants of old unsafe system; do not crash, silently stop - return - } - val user = optUser.get() + // remnants of old unsafe system; do not crash, silently stop + val currentUser = user.orElse(null) ?: return + if (showPreview) { - val fragment = PreviewTextFileFragment.create(user, file, searchOpen, searchQuery) + val fragment = PreviewTextFileFragment.create(currentUser, file, searchOpen, searchQuery) setLeftFragment(fragment, false) configureToolbarForPreview(file) showBottomNavigationBar(false) } else { - val previewIntent = Intent() - previewIntent.putExtra(EXTRA_FILE, file) - previewIntent.putExtra(TEXT_PREVIEW, true) - val fileOperationsHelper = - FileOperationsHelper(this, userAccountManager, connectivityService, editorUtils) - fileOperationsHelper.startSyncForFileAndIntent(file, previewIntent) + startSyncForFileAndIntent(file, previewIntentFor(file, TEXT_PREVIEW)) } } @@ -2830,30 +2585,28 @@ class FileDisplayActivity : * @param folder [OCFile] to preview its rich workspace. */ fun startRichWorkspacePreview(folder: OCFile?) { - val args = Bundle() - args.putParcelable(EXTRA_FILE, folder) configureToolbarForPreview(folder) - val textPreviewFragment = - Fragment.instantiate(applicationContext, PreviewTextStringFragment::class.java.name, args) + val textPreviewFragment = PreviewTextStringFragment().apply { + arguments = Bundle().apply { putParcelable(EXTRA_FILE, folder) } + } setLeftFragment(textPreviewFragment, false) } fun startContactListFragment(file: OCFile?) { - val user = user.orElseThrow(Supplier { RuntimeException() }) - ContactsPreferenceActivity.startActivityWithContactsFile(this, user, file) + val currentUser = user.orElse(null) ?: return + ContactsPreferenceActivity.startActivityWithContactsFile(this, currentUser, file) } fun startPdfPreview(file: OCFile) { + // prefer third party PDF apps if (fileOperationsHelper.canOpenFile(file)) { - // prefer third party PDF apps fileOperationsHelper.openFile(file) - } else { - val pdfFragment: Fragment = newInstance(file) - - setLeftFragment(pdfFragment, false) - configureToolbarForPreview(file) - setMainFabVisible(false) + return } + + setLeftFragment(PreviewPdfFragment.newInstance(file), false) + configureToolbarForPreview(file) + setMainFabVisible(false) } /** @@ -2865,15 +2618,13 @@ class FileDisplayActivity : */ fun startDownloadForPreview(file: OCFile, parentFolder: OCFile?) { if (!file.isFileEligibleForImmediatePreview) { - val currentUser = user - if (currentUser.isPresent) { - val detailFragment: Fragment = FileDetailFragment.newInstance(file, parentFolder, currentUser.get()) - setLeftFragment(detailFragment, false) + user.orElse(null)?.let { + setLeftFragment(FileDetailFragment.newInstance(file, parentFolder, it), false) } } configureToolbarForPreview(file) - mWaitingToPreview = file + waitingToPreview = file requestForDownload() setFile(file) } @@ -2885,19 +2636,16 @@ class FileDisplayActivity : * @param file [OCFile] (image) to be loaded into image editor */ fun startImageEditor(file: OCFile) { - if (file.isDown) { - val editImageIntent = Intent(this, EditImageActivity::class.java) - editImageIntent.putExtra(EditImageActivity.EXTRA_FILE, file) - startActivity(editImageIntent) - } else { - mWaitingToPreview = file - requestForDownload( - file, - EditImageActivity.OPEN_IMAGE_EDITOR, - packageName, - this.javaClass.simpleName - ) + if (!file.isDown) { + waitingToPreview = file + requestForDownload(file, EditImageActivity.OPEN_IMAGE_EDITOR, packageName, javaClass.simpleName) + return + } + + val editImageIntent = Intent(this, EditImageActivity::class.java).apply { + putExtra(EditImageActivity.EXTRA_FILE, file) } + startActivity(editImageIntent) } /** @@ -2907,11 +2655,11 @@ class FileDisplayActivity : */ fun cancelTransference(file: OCFile) { fileOperationsHelper.cancelTransference(file) - if (mWaitingToPreview != null && mWaitingToPreview?.remotePath == file.remotePath) { - mWaitingToPreview = null + if (waitingToPreview?.remotePath == file.remotePath) { + waitingToPreview = null } - if (mWaitingToSend != null && mWaitingToSend?.remotePath == file.remotePath) { - mWaitingToSend = null + if (waitingToSend?.remotePath == file.remotePath) { + waitingToSend = null } onTransferStateChanged(file, false, false) } @@ -2922,9 +2670,7 @@ class FileDisplayActivity : * @param files collection of [OCFile] files which operations are wanted to be cancel */ fun cancelTransference(files: MutableCollection) { - for (file in files) { - cancelTransference(file) - } + files.forEach { cancelTransference(it) } } override fun onRefresh(ignoreETag: Boolean) { @@ -2936,10 +2682,8 @@ class FileDisplayActivity : } private fun syncAndUpdateFolder(ignoreETag: Boolean, ignoreFocus: Boolean) { - val listOfFiles = this.listOfFilesFragment - if (listOfFiles == null || listOfFiles.isSearchFragment) { - return - } + val listOfFiles = listOfFilesFragment ?: return + if (listOfFiles.isSearchFragment) return val folder = listOfFiles.currentFile ?: return startSyncFolderOperation(folder, ignoreETag, ignoreFocus) @@ -2951,28 +2695,35 @@ class FileDisplayActivity : } private fun refreshOrInitOCFileListFragment() { - val ocFileListFragment = this.listOfFilesFragment + val ocFileListFragment = listOfFilesFragment if (ocFileListFragment != null && - (ocFileListFragment !is GalleryFragment) && - (ocFileListFragment !is SharedListFragment) && + ocFileListFragment !is GalleryFragment && + ocFileListFragment !is SharedListFragment && // album fragment check will help in showing offline files screen // when navigating from Albums to Offline Files - !isAlbumsFragment && !isAlbumItemsFragment + !isAlbumsFragment && + !isAlbumItemsFragment ) { ocFileListFragment.refreshDirectory() } else { - this.leftFragment = OCFileListFragment() + leftFragment = OCFileListFragment() } } @Subscribe(threadMode = ThreadMode.BACKGROUND) fun onMessageEvent(event: SearchEvent) { - if (SearchRemoteOperation.SearchType.PHOTO_SEARCH == event.searchType) { - Log_OC.d(this, "Switch to photo search fragment") - this.leftFragment = GalleryFragment() - } else if (event.searchType == SearchRemoteOperation.SearchType.SHARED_FILTER) { - Log_OC.d(this, "Switch to Shared fragment") - this.leftFragment = SharedListFragment() + when (event.searchType) { + SearchRemoteOperation.SearchType.PHOTO_SEARCH -> { + Log_OC.d(this, "Switch to photo search fragment") + leftFragment = GalleryFragment() + } + + SearchRemoteOperation.SearchType.SHARED_FILTER -> { + Log_OC.d(this, "Switch to Shared fragment") + leftFragment = SharedListFragment() + } + + else -> Unit } listOfFilesFragment?.updateSearchType(event) @@ -2981,24 +2732,24 @@ class FileDisplayActivity : @Subscribe(threadMode = ThreadMode.MAIN) fun onMessageEvent(event: SyncEventFinished) { - val bundle = event.intent.extras - val file = bundle?.get(EXTRA_FILE) as OCFile? ?: return - - if (event.intent.getBooleanExtra(TEXT_PREVIEW, false)) { - startTextPreview(file, true) - } else if (event.intent.getBooleanExtra(AUDIO_PREVIEW, false)) { - startAudioPreview(file, true, true) - } else if (bundle.containsKey(PreviewMediaActivity.EXTRA_VIRTUAL_TYPE)) { - val virtualType = bundle.get(PreviewMediaActivity.EXTRA_VIRTUAL_TYPE) as VirtualFolderType? - startImagePreview( + val intent = event.intent + val bundle = intent.extras ?: return + val file = bundle.getParcelableArgument(EXTRA_FILE, OCFile::class.java) ?: return + + when { + intent.getBooleanExtra(TEXT_PREVIEW, false) -> startTextPreview(file, true) + + intent.getBooleanExtra(AUDIO_PREVIEW, false) -> startAudioPreview(file, true, true) + + bundle.containsKey(PreviewMediaActivity.EXTRA_VIRTUAL_TYPE) -> startImagePreview( file, true, - virtualType, + bundle.getSerializableArgument(PreviewMediaActivity.EXTRA_VIRTUAL_TYPE, VirtualFolderType::class.java), bundle.getSerializableArgument(PreviewMediaActivity.EXTRA_MEDIA_STATE, MediaState::class.java), galleryThumbnailOf(file) ) - } else { - startImagePreview(file, true, sourceView = galleryThumbnailOf(file)) + + else -> startImagePreview(file, true, sourceView = galleryThumbnailOf(file)) } } @@ -3011,10 +2762,10 @@ class FileDisplayActivity : @Subscribe(threadMode = ThreadMode.BACKGROUND) fun onMessageEvent(event: TokenPushEvent?) { - if (!preferences.isKeysReInitEnabled()) { - PushUtils.reinitKeys(userAccountManager) - } else { + if (preferences.isKeysReInitEnabled()) { PushUtils.pushRegistrationToServer(userAccountManager, preferences.getPushToken()) + } else { + PushUtils.reinitKeys(userAccountManager) } } @@ -3031,47 +2782,18 @@ class FileDisplayActivity : initFile() } - @Suppress("LongMethod") private fun initFile() { - val userOpt = user - if (userOpt.isEmpty) { + val existingUser = user.orElse(null) ?: run { Log_OC.w(TAG, "user is not available, cannot init file") return } - val existingUser = userOpt.get() - val storageManager = storageManager - if (storageManager == null) { + val currentStorageManager: FileDataStorageManager = storageManager ?: run { Log_OC.w(TAG, "storage manager is null, cannot init file") return } - var file = getFile() - if (file != null) { - if (file.isDown && file.lastSyncDateForProperties == 0L) { - val remote = file.remotePath - val name = file.fileName - - val idx = remote.lastIndexOf(name) - if (idx > 0) { - val parentPath = remote.take(idx) - if (storageManager.getFileByPath(parentPath) == null) { - file = null - } - } else { - file = null - } - } else { - file = storageManager.getFileByPath(file.remotePath) - } - } - - // fall back to root folder - if (file == null) { - file = storageManager.getFileByPath(OCFile.ROOT_PATH) - } - - if (file == null) { + val file = resolveInitialFile(currentStorageManager) ?: run { Log_OC.e(TAG, "Could not retrieve root folder – cannot continue") return } @@ -3084,15 +2806,15 @@ class FileDisplayActivity : avatarGenerator.setAccountAvatar( existingUser, this, - getResources().getDimension(R.dimen.nav_drawer_menu_avatar_radius), + resources.getDimension(R.dimen.nav_drawer_menu_avatar_radius), mSwitchAccountButton ) - val userChanged = (existingAccountName != lastDisplayedAccountName) + val userChanged = existingAccountName != lastDisplayedAccountName if (userChanged) { composeProcessTextAlias.configure() Log_OC.d(TAG, "Initializing Fragments in onAccountChanged..") initFragments() - if (file.isFolder && TextUtils.isEmpty(searchQuery)) { + if (file.isFolder && searchQuery.isNullOrEmpty()) { startSyncFolderOperation(file, false) } } else { @@ -3104,6 +2826,21 @@ class FileDisplayActivity : checkForNewDevVersionNecessary(applicationContext) } + private fun resolveInitialFile(storageManager: FileDataStorageManager): OCFile? = + getFile()?.let { resolveCurrentFile(it, storageManager) } ?: storageManager.getFileByPath(OCFile.ROOT_PATH) + + private fun resolveCurrentFile(file: OCFile, storageManager: FileDataStorageManager): OCFile? { + if (!file.isDown || file.lastSyncDateForProperties != 0L) { + return storageManager.getFileByPath(file.remotePath) + } + + val parentPathLength = file.remotePath.lastIndexOf(file.fileName) + if (parentPathLength <= 0) return null + + val parentPath = file.remotePath.take(parentPathLength) + return file.takeIf { storageManager.getFileByPath(parentPath) != null } + } + private fun setNewLastDisplayedAccountName(accountName: String) { preferences.lastDisplayedAccountName = accountName lastDisplayedAccountName = accountName @@ -3128,47 +2865,39 @@ class FileDisplayActivity : val intentData = intent.data if (userName == null && fileId == null && intentData != null) { openDeepLink(intentData) - } else { - val optionalUser = if (userName == null) user else userAccountManager.getUser(userName) - if (optionalUser.isPresent) { - if (!TextUtils.isEmpty(fileId)) { - openFile(optionalUser.get(), fileId) - } else if (!TextUtils.isEmpty(filePath)) { - openFileByPath(optionalUser.get(), filePath) - } else { - accountClicked(optionalUser.get()) - } - } else { - SnackbarUtil.show(this, getString(R.string.associated_account_not_found)) - } + return + } + + val optionalUser = if (userName == null) user else userAccountManager.getUser(userName) + val targetUser = optionalUser.orElse(null) ?: run { + SnackbarUtil.show(this, getString(R.string.associated_account_not_found)) + return + } + + when { + !fileId.isNullOrEmpty() -> openFile(targetUser, fileId) + !filePath.isNullOrEmpty() -> openFileByPath(targetUser, filePath) + else -> accountClicked(targetUser) } } private fun openDeepLink(uri: Uri) { - val linkHandler = DeepLinkHandler(userAccountManager) - val match = linkHandler.parseDeepLink(uri) + val match = DeepLinkHandler(userAccountManager).parseDeepLink(uri) - if (match == null) { - handleDeepLink(uri) - } else if (match.users.isEmpty()) { - SnackbarUtil.show(this, getString(R.string.associated_account_not_found)) - } else if (match.users.size == SINGLE_USER_SIZE) { - openFile(match.users[0], match.fileId) - } else { - selectUserAndOpenFile(match.users.toMutableList(), match.fileId) + when { + match == null -> handleDeepLink(uri) + match.users.isEmpty() -> SnackbarUtil.show(this, getString(R.string.associated_account_not_found)) + match.users.size == SINGLE_USER_SIZE -> openFile(match.users.first(), match.fileId) + else -> selectUserAndOpenFile(match.users, match.fileId) } } - private fun selectUserAndOpenFile(users: MutableList, fileId: String?) { - val userNames = arrayOfNulls(users.size) - for (i in userNames.indices) { - userNames[i] = users[i]?.accountName - } + private fun selectUserAndOpenFile(users: List, fileId: String?) { + val userNames = users.map { it.accountName }.toTypedArray() val builder = MaterialAlertDialogBuilder(this) - builder.setTitle(R.string.common_choose_account) - .setItems(userNames) { dialog: DialogInterface?, which: Int -> - val user = users[which] - openFile(user, fileId) + .setTitle(R.string.common_choose_account) + .setItems(userNames) { _, which -> + openFile(users[which], fileId) showLoadingDialog(getString(R.string.retrieving_file)) } @@ -3187,24 +2916,19 @@ class FileDisplayActivity : return } - var storageManager = getStorageManager() - - if (storageManager == null) { - storageManager = FileDataStorageManager(user, contentResolver) - } - if (user == null) { Log_OC.e(TAG, "cannot fetch remote file, user is null") return } - if (capabilities.isEmpty) { + val capability = capabilities.orElse(null) ?: run { Log_OC.e(TAG, "cannot fetch remote file, capabilities is empty") return } - val weakContext: WeakReference = WeakReference(this) - val task = FetchRemoteFileTask(user, fileId, storageManager, lifecycleScope, capabilities.get(), weakContext) + val fileStorageManager = storageManager ?: FileDataStorageManager(user, contentResolver) + val weakContext = WeakReference(this) + val task = FetchRemoteFileTask(user, fileId, fileStorageManager, lifecycleScope, capability, weakContext) task.run(showFile = { (ocFile, message) -> ocFile?.let { file = it } showFile(ocFile, message) @@ -3219,25 +2943,19 @@ class FileDisplayActivity : return } - var storageManager = getStorageManager() - - if (storageManager == null) { - storageManager = FileDataStorageManager(user, contentResolver) - } - - val client: OwnCloudClient - try { - client = clientFactory.create(user) + val fileStorageManager = storageManager ?: FileDataStorageManager(user, contentResolver) + val client = try { + clientFactory.create(user) } catch (_: CreationException) { onFileRequestError(null) return } - val getRemoteFileTask = GetRemoteFileTask(this, filepath, client, storageManager, user) + val getRemoteFileTask = GetRemoteFileTask(this, filepath, client, fileStorageManager, user) asyncRunner.postQuickTask( getRemoteFileTask, - { result: GetRemoteFileTask.Result -> this.onFileRequestResult(result) }, - { throwable: Throwable? -> this.onFileRequestError(throwable) } + { result: GetRemoteFileTask.Result -> onFileRequestResult(result) }, + { throwable: Throwable? -> onFileRequestError(throwable) } ) } @@ -3253,7 +2971,7 @@ class FileDisplayActivity : file = result.file val fileFragment = OCFileListFragment() - this.leftFragment = fileFragment + leftFragment = fileFragment supportFragmentManager.executePendingTransactions() @@ -3271,27 +2989,24 @@ class FileDisplayActivity : } fun setMainFabVisible(visible: Boolean) { - val visibility = if (visible) View.VISIBLE else View.GONE - binding.fabMain.visibility = visibility + binding.fabMain.isVisible = visible } fun showFile(selectedFile: OCFile?, message: String?) { - getOCFileListFragmentFromFile(object : TransactionInterface { - override fun onOCFileListFragmentComplete(fragment: OCFileListFragment) { - dismissLoadingDialog() - - if (message.isNullOrEmpty()) { - val current = getCurrentDir() - fragment.listDirectory(current, file, MainApp.isOnlyOnDevice()) - file = current - updateActionBarTitleAndHomeButton(null) - } else { - fragment.view?.let { SnackbarUtil.show(it, message) } - } + getOCFileListFragmentFromFile { fragment -> + dismissLoadingDialog() - selectedFile?.let(fragment::onItemClicked) + if (message.isNullOrEmpty()) { + val current = getCurrentDir() + fragment.listDirectory(current, file, MainApp.isOnlyOnDevice()) + file = current + updateActionBarTitleAndHomeButton(null) + } else { + fragment.view?.let { SnackbarUtil.show(it, message) } } - }) + + selectedFile?.let(fragment::onItemClicked) + } } override fun onFilesRemoved() { @@ -3303,14 +3018,13 @@ class FileDisplayActivity : intent, object : AccountReceiverCallback { override fun onAccountReceived(accountName: String) { - val account = accountManager.getUser(accountName).orElse(null) - ?: run { - Log_OC.w(TAG, "user is not present") - SnackbarUtil.show(this@FileDisplayActivity, R.string.account_not_found) - return - } - - accountClicked(account) + val receivedUser = accountManager.getUser(accountName).orElse(null) ?: run { + Log_OC.w(TAG, "user is not present") + SnackbarUtil.show(this@FileDisplayActivity, R.string.account_not_found) + return + } + + accountClicked(receivedUser) } override fun onAccountError(reason: String) { @@ -3332,20 +3046,20 @@ class FileDisplayActivity : // endregion companion object { - const val RESTART: String = "RESTART" - const val ALL_FILES: String = "ALL_FILES" - const val LIST_GROUPFOLDERS: String = "LIST_GROUPFOLDERS" - const val ALBUMS: String = "ALBUMS" - const val SINGLE_USER_SIZE: Int = 1 - const val OPEN_FILE: String = "NC_OPEN_FILE" + const val RESTART = "RESTART" + const val ALL_FILES = "ALL_FILES" + const val LIST_GROUPFOLDERS = "LIST_GROUPFOLDERS" + const val ALBUMS = "ALBUMS" + const val SINGLE_USER_SIZE = 1 + const val OPEN_FILE = "NC_OPEN_FILE" const val ON_DEVICE = "ON_DEVICE" - const val TAG_PUBLIC_LINK: String = "PUBLIC_LINK" - const val FTAG_CHOOSER_DIALOG: String = "CHOOSER_DIALOG" - const val KEY_FILE_ID: String = "KEY_FILE_ID" - const val KEY_FILE_PATH: String = "KEY_FILE_PATH" - const val KEY_ACCOUNT: String = "KEY_ACCOUNT" - const val KEY_IS_SORT_GROUP_VISIBLE: String = "KEY_IS_SORT_GROUP_VISIBLE" + const val TAG_PUBLIC_LINK = "PUBLIC_LINK" + const val FTAG_CHOOSER_DIALOG = "CHOOSER_DIALOG" + const val KEY_FILE_ID = "KEY_FILE_ID" + const val KEY_FILE_PATH = "KEY_FILE_PATH" + const val KEY_ACCOUNT = "KEY_ACCOUNT" + const val KEY_IS_SORT_GROUP_VISIBLE = "KEY_IS_SORT_GROUP_VISIBLE" private const val KEY_WAITING_TO_PREVIEW = "WAITING_TO_PREVIEW" private const val KEY_SYNC_STATE = "SYNC_STATE" @@ -3354,44 +3068,56 @@ class FileDisplayActivity : private const val ON_RESUMED_RESET_DELAY = 10000L private const val SEARCH_VIEW_FOCUS_DELAY = 100L + private const val NO_IMMEDIATE_PREVIEW_FILE_ID = -1L + private const val UNKNOWN_VISIBILITY = -1 + private const val NO_NEUTRAL_BUTTON = -1 - const val ACTION_DETAILS: String = "com.owncloud.android.ui.activity.action.DETAILS" - const val EXTRA_RETURN_TO_PREVIEW: String = "RETURN_TO_PREVIEW" + const val ACTION_DETAILS = "com.owncloud.android.ui.activity.action.DETAILS" + const val EXTRA_RETURN_TO_PREVIEW = "RETURN_TO_PREVIEW" - @JvmField - val REQUEST_CODE__SELECT_CONTENT_FROM_APPS: Int = REQUEST_CODE__LAST_SHARED + 1 + val REQUEST_CODE__SELECT_CONTENT_FROM_APPS = REQUEST_CODE__LAST_SHARED + 1 - @JvmField - val REQUEST_CODE__SELECT_FILES_FROM_FILE_SYSTEM: Int = REQUEST_CODE__LAST_SHARED + 2 + val REQUEST_CODE__SELECT_FILES_FROM_FILE_SYSTEM = REQUEST_CODE__LAST_SHARED + 2 - @JvmField - val REQUEST_CODE__MOVE_OR_COPY_FILES: Int = REQUEST_CODE__LAST_SHARED + 3 + val REQUEST_CODE__MOVE_OR_COPY_FILES = REQUEST_CODE__LAST_SHARED + 3 - @JvmField - val REQUEST_CODE__UPLOAD_FROM_CAMERA: Int = REQUEST_CODE__LAST_SHARED + 5 + val REQUEST_CODE__UPLOAD_FROM_CAMERA = REQUEST_CODE__LAST_SHARED + 5 - @JvmField - val REQUEST_CODE__UPLOAD_FROM_VIDEO_CAMERA: Int = REQUEST_CODE__LAST_SHARED + 6 + val REQUEST_CODE__UPLOAD_FROM_VIDEO_CAMERA = REQUEST_CODE__LAST_SHARED + 6 - @JvmField - val REQUEST_CODE__SELECT_CONTENT_FROM_APPS_AUTO_RENAME: Int = REQUEST_CODE__LAST_SHARED + 7 + val REQUEST_CODE__SELECT_CONTENT_FROM_APPS_AUTO_RENAME = REQUEST_CODE__LAST_SHARED + 7 + + private val selectContentFromAppsRequestCodes = setOf( + REQUEST_CODE__SELECT_CONTENT_FROM_APPS, + REQUEST_CODE__SELECT_CONTENT_FROM_APPS_AUTO_RENAME + ) + private val selectContentFromAppsResultCodes = setOf(RESULT_OK, UploadFilesActivity.RESULT_OK_AND_MOVE) + private val selectFilesFromFileSystemResultCodes = setOf( + RESULT_OK, + UploadFilesActivity.RESULT_OK_AND_MOVE, + UploadFilesActivity.RESULT_OK_AND_DO_NOTHING, + UploadFilesActivity.RESULT_OK_AND_DELETE + ) + private val uploadFromCameraRequestCodes = setOf( + REQUEST_CODE__UPLOAD_FROM_CAMERA, + REQUEST_CODE__UPLOAD_FROM_VIDEO_CAMERA + ) + private val uploadFromCameraResultCodes = setOf(RESULT_OK, UploadFilesActivity.RESULT_OK_AND_DELETE) - private val TAG: String = FileDisplayActivity::class.java.getSimpleName() + private val TAG: String = FileDisplayActivity::class.java.simpleName - const val TAG_LIST_OF_FILES: String = "LIST_OF_FILES" + const val TAG_LIST_OF_FILES = "LIST_OF_FILES" - const val TEXT_PREVIEW: String = "TEXT_PREVIEW" - const val AUDIO_PREVIEW: String = "AUDIO_PREVIEW" + const val TEXT_PREVIEW = "TEXT_PREVIEW" + const val AUDIO_PREVIEW = "AUDIO_PREVIEW" - const val KEY_IS_SEARCH_OPEN: String = "IS_SEARCH_OPEN" - const val KEY_SEARCH_QUERY: String = "SEARCH_QUERY" + const val KEY_IS_SEARCH_OPEN = "IS_SEARCH_OPEN" + const val KEY_SEARCH_QUERY = "SEARCH_QUERY" - @JvmStatic - fun openFileIntent(context: Context?, user: User?, file: OCFile?): Intent { - val intent = Intent(context, PreviewMediaActivity::class.java) - intent.putExtra(EXTRA_FILE, file) - intent.putExtra(EXTRA_USER, user) - return intent - } + fun openFileIntent(context: Context?, user: User?, file: OCFile?): Intent = + Intent(context, PreviewMediaActivity::class.java).apply { + putExtra(EXTRA_FILE, file) + putExtra(EXTRA_USER, user) + } } } From ec37270835c8691836b12fea8d2188d01ad65809 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 29 Oct 2026 09:09:26 +0300 Subject: [PATCH 2/3] refactor Signed-off-by: alperozturk96 --- .../com/owncloud/android/ui/activity/FileDisplayActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 fd75cf37a1fa..4ea0cff45f1f 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 @@ -3114,7 +3114,7 @@ class FileDisplayActivity : const val KEY_IS_SEARCH_OPEN = "IS_SEARCH_OPEN" const val KEY_SEARCH_QUERY = "SEARCH_QUERY" - fun openFileIntent(context: Context?, user: User?, file: OCFile?): Intent = + fun openFileIntent(context: Context, user: User, file: OCFile): Intent = Intent(context, PreviewMediaActivity::class.java).apply { putExtra(EXTRA_FILE, file) putExtra(EXTRA_USER, user) From 8f25744c9337c702ebe02f37bb160fb9174cf78e Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 29 Oct 2026 09:25:18 +0300 Subject: [PATCH 3/3] simplify Signed-off-by: alperozturk96 --- .../ui/activity/FileDisplayActivity.kt | 3874 ++++++++--------- .../DownloadServiceConnection.kt | 28 + .../filedisplayactivity/FileDisplayDialogs.kt | 96 + .../FileDisplayUploadHandler.kt | 192 + .../FileDownloadIndicator.kt | 15 + .../LocalIntentReceiver.kt | 15 + .../fragment/albums/AlbumOperationListener.kt | 4 +- 7 files changed, 2051 insertions(+), 2173 deletions(-) create mode 100644 app/src/main/java/com/owncloud/android/ui/activity/filedisplayactivity/DownloadServiceConnection.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/activity/filedisplayactivity/FileDisplayDialogs.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/activity/filedisplayactivity/FileDisplayUploadHandler.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/activity/filedisplayactivity/FileDownloadIndicator.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/activity/filedisplayactivity/LocalIntentReceiver.kt diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt index 4ea0cff45f1f..1e1584ffb0f4 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 @@ -17,7 +17,6 @@ package com.owncloud.android.ui.activity import android.accounts.AuthenticatorException import android.annotation.SuppressLint -import android.content.BroadcastReceiver import android.content.ComponentName import android.content.Context import android.content.Intent @@ -31,20 +30,17 @@ import android.os.Build import android.os.Bundle import android.os.Environment import android.os.Handler -import android.os.IBinder import android.os.Looper import android.os.Parcelable import android.view.Menu import android.view.MenuItem import android.view.View import android.view.ViewTreeObserver.OnGlobalLayoutListener -import android.view.WindowManager.BadTokenException import android.view.inputmethod.InputMethodManager import androidx.activity.OnBackPressedCallback import androidx.activity.viewModels import androidx.annotation.VisibleForTesting import androidx.appcompat.widget.SearchView -import androidx.core.util.Function import androidx.core.view.ViewCompat import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams @@ -54,14 +50,11 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.localbroadcastmanager.content.LocalBroadcastManager import com.google.android.material.appbar.AppBarLayout -import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import com.nextcloud.android.common.core.utils.ecosystem.AccountReceiverCallback import com.nextcloud.appReview.InAppReviewHelper import com.nextcloud.client.account.User -import com.nextcloud.client.appinfo.AppInfo import com.nextcloud.client.core.AsyncRunner -import com.nextcloud.client.core.Clock import com.nextcloud.client.database.entity.SyncedFolderEntity import com.nextcloud.client.di.Injectable import com.nextcloud.client.di.ViewModelFactory @@ -69,26 +62,19 @@ import com.nextcloud.client.editimage.EditImageActivity import com.nextcloud.client.files.DeepLinkHandler import com.nextcloud.client.jobs.download.FileDownloadEventBroadcaster import com.nextcloud.client.jobs.download.FileDownloadHelper -import com.nextcloud.client.jobs.download.FileDownloadWorker import com.nextcloud.client.jobs.upload.FileUploadEventBroadcaster -import com.nextcloud.client.jobs.upload.FileUploadHelper -import com.nextcloud.client.jobs.upload.FileUploadWorker -import com.nextcloud.client.network.ClientFactory.CreationException import com.nextcloud.client.player.model.file.toPlaybackCollection import com.nextcloud.client.player.ui.audio.AudioPlayerLauncher import com.nextcloud.client.preferences.AppPreferences import com.nextcloud.client.utils.IntentUtil -import com.nextcloud.model.OCUploadLocalPathData import com.nextcloud.ui.composeActivity.ComposeProcessTextAlias import com.nextcloud.utils.SnackbarUtil import com.nextcloud.utils.extensions.getParcelableArgument import com.nextcloud.utils.extensions.getSerializableArgument import com.nextcloud.utils.extensions.isActive -import com.nextcloud.utils.extensions.isDialogFragmentReady import com.nextcloud.utils.extensions.lastFragment import com.nextcloud.utils.extensions.navigateToAllFiles import com.nextcloud.utils.extensions.setVisibleIf -import com.nextcloud.utils.fileNameValidator.FileNameValidator.checkFolderPath import com.nextcloud.utils.view.FastScrollUtils import com.owncloud.android.MainApp import com.owncloud.android.R @@ -120,17 +106,18 @@ import com.owncloud.android.operations.SynchronizeFileOperation import com.owncloud.android.operations.albums.CopyFileToAlbumOperation import com.owncloud.android.syncadapter.FileSyncAdapter import com.owncloud.android.ui.CompletionCallback +import com.owncloud.android.ui.activity.filedisplayactivity.DownloadServiceConnection import com.owncloud.android.ui.activity.filedisplayactivity.FileDisplayActivityViewModel -import com.owncloud.android.ui.asynctasks.CheckAvailableSpaceTask -import com.owncloud.android.ui.asynctasks.CheckAvailableSpaceTask.CheckAvailableSpaceListener +import com.owncloud.android.ui.activity.filedisplayactivity.FileDisplayDialogs +import com.owncloud.android.ui.activity.filedisplayactivity.FileDisplayUploadHandler +import com.owncloud.android.ui.activity.filedisplayactivity.FileDownloadIndicator +import com.owncloud.android.ui.activity.filedisplayactivity.LocalIntentReceiver import com.owncloud.android.ui.asynctasks.FetchRemoteFileTask import com.owncloud.android.ui.asynctasks.GetRemoteFileTask -import com.owncloud.android.ui.dialog.ConfirmationDialogFragment import com.owncloud.android.ui.dialog.DeleteBatchTracker import com.owncloud.android.ui.dialog.SendShareDialog.SendShareDialogDownloader import com.owncloud.android.ui.dialog.SortingOrderDialogFragment.OnSortingOrderListener import com.owncloud.android.ui.dialog.StoragePermissionDialogFragment -import com.owncloud.android.ui.dialog.TermsOfServiceDialog import com.owncloud.android.ui.events.SearchEvent import com.owncloud.android.ui.events.SyncEventFinished import com.owncloud.android.ui.events.TokenPushEvent @@ -149,15 +136,14 @@ import com.owncloud.android.ui.fragment.albums.AlbumItemsFragment import com.owncloud.android.ui.fragment.albums.AlbumOperationListener import com.owncloud.android.ui.fragment.albums.AlbumsFragment import com.owncloud.android.ui.helpers.FileOperationsHelper -import com.owncloud.android.ui.helpers.UriUploader import com.owncloud.android.ui.navigation.NavigatorScreen import com.owncloud.android.ui.navigation.animator.NavigationAnimator import com.owncloud.android.ui.preview.media.PreviewMediaActivity import com.owncloud.android.ui.preview.media.PreviewMediaFragment +import com.owncloud.android.ui.preview.pdf.PreviewPdfFragment import com.owncloud.android.ui.preview.text.PreviewTextFileFragment import com.owncloud.android.ui.preview.text.PreviewTextFragment import com.owncloud.android.ui.preview.text.PreviewTextStringFragment -import com.owncloud.android.ui.preview.pdf.PreviewPdfFragment import com.owncloud.android.utils.DataHolderUtil import com.owncloud.android.utils.ErrorMessageAdapter import com.owncloud.android.utils.FileSortOrder @@ -166,13 +152,10 @@ import com.owncloud.android.utils.PermissionUtil import com.owncloud.android.utils.PermissionUtil.requestNotificationPermission import com.owncloud.android.utils.PermissionUtil.requestStoragePermissionIfNeeded import com.owncloud.android.utils.PushUtils -import com.owncloud.android.utils.StringUtils -import com.owncloud.android.utils.UriUtils import com.owncloud.android.utils.theme.CapabilityUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.apache.commons.io.FilenameUtils import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode @@ -181,7 +164,7 @@ import java.lang.ref.WeakReference import javax.inject.Inject /** - * Displays, what files the user has available in his Nextcloud. This is the main view. + * Main screen, lists the files of the current account. */ @Suppress("ComplexCondition", "ForbiddenComment", "LargeClass") class FileDisplayActivity : @@ -192,43 +175,8 @@ class FileDisplayActivity : SendShareDialogDownloader, OnFilesRemovedListener, Injectable { - private lateinit var binding: FilesBinding - - private val syncReceiver = SyncReceiver() - private val fileUploadCompletedReceiver = FileUploadCompletedReceiver() - - private val fileDownloadStartedReceiver = FileDownloadStartedReceiver() - private val fileDownloadCompletedReceiver = FileDownloadCompletedReceiver() - - private lateinit var albumOperationListener: AlbumOperationListener - - var mLastSslUntrustedServerResult: RemoteOperationResult<*>? = null - - private var waitingToPreview: OCFile? = null - - private var syncState: Parcelable = EmptyListState.LOADING - set(value) { - field = value - listOfFilesFragment?.setEmptyListMessage(value) - } - - private var pendingSyncFolderOperation: Runnable? = null - - private var waitingToSend: OCFile? = null - - private var drawerMenuItemsToShowHide: List = emptyList() - - private var searchQuery: String? = "" - private var searchOpen = false - - private var searchView: SearchView? = null - private var lastDisplayedAccountName: String? = null - - // needed for first time app launch multiple listing directory call - // because onActivityCreated causes this. Removing list directory call from onActivityCreated - // causing also empty state thus this flag is used. - private var listFragmentJustCreated = false + // region Properties @Inject lateinit var audioPlayerLauncher: AudioPlayerLauncher @@ -241,9 +189,6 @@ class FileDisplayActivity : @Inject lateinit var preferences: AppPreferences - @Inject - lateinit var appInfo: AppInfo - @Inject lateinit var inAppReviewHelper: InAppReviewHelper @@ -253,9 +198,6 @@ class FileDisplayActivity : @Inject lateinit var asyncRunner: AsyncRunner - @Inject - lateinit var clock: Clock - @Inject lateinit var syncedFolderProvider: SyncedFolderProvider @@ -267,138 +209,187 @@ class FileDisplayActivity : private val viewModel by viewModels { viewModelFactory } - /** - * Indicates whether the downloaded file should be previewed immediately. Since `FileDownloadWorker` can be - * triggered from multiple sources, this helps determine if an automatic preview is needed after download. - */ - private var fileIDForImmediatePreview: Long = NO_IMMEDIATE_PREVIEW_FILE_ID + private lateinit var binding: FilesBinding + private lateinit var albumOperationListener: AlbumOperationListener + private lateinit var folderRefreshScheduler: FolderRefreshScheduler - private var isShowingDetailsFromPreview = false + private val dialogs by lazy { FileDisplayDialogs(this, viewThemeUtils) } + private val uploadHandler by lazy { FileDisplayUploadHandler(this, connectivityService, fileDataStorageManager) } - private lateinit var folderRefreshScheduler: FolderRefreshScheduler + private val syncReceiver = LocalIntentReceiver(::onSyncBroadcastReceived) + private val uploadCompletedReceiver = LocalIntentReceiver(::onUploadCompleted) + private val downloadStartedReceiver = LocalIntentReceiver(::onDownloadStarted) + private val downloadCompletedReceiver = LocalIntentReceiver(::onDownloadCompleted) - override fun setFile(file: OCFile?) { - super.setFile(file) - file?.takeIf { it.isFolder }?.let { viewModel.setCurrentFolder(it.fileId) } - } + var lastSslUntrustedServerResult: RemoteOperationResult<*>? = null - fun setFileIDForImmediatePreview(fileIDForImmediatePreview: Long) { - this.fileIDForImmediatePreview = fileIDForImmediatePreview - } + private var waitingToPreview: OCFile? = null + private var waitingToSend: OCFile? = null + + private var syncState: Parcelable = EmptyListState.LOADING + set(value) { + field = value + listOfFilesFragment?.setEmptyListMessage(value) + } + + private var pendingSyncFolderOperation: Runnable? = null + + private var drawerMenuItemsToShowHide: List = emptyList() + private var searchView: SearchView? = null + private var searchQuery = "" + private var wasSearchOpen = false + + private var lastDisplayedAccountName = "" + + // The list fragment lists its directory when it is created, so onResume must not list it a second time. + private var listFragmentJustCreated = false + + // FileDownloadWorker is started from many places, only this file is previewed right after its download. + private var immediatePreviewFileId = NO_IMMEDIATE_PREVIEW_FILE_ID + + private var isShowingDetailsFromPreview = false + + val deleteBatchTracker = DeleteBatchTracker(onAllDeletesFinished = { + (leftFragment as? GalleryFragment)?.onRefresh() + }) + // endregion + // region Lifecycle @SuppressLint("UnsafeIntentLaunch") override fun onCreate(savedInstanceState: Bundle?) { Log_OC.v(TAG, "onCreate() start") - // Set the default theme to replace the launch screen theme. + // replaces the launch screen theme setTheme(R.style.Theme_ownCloud_Toolbar_Drawer) super.onCreate(savedInstanceState) - lastDisplayedAccountName = preferences.lastDisplayedAccountName + lastDisplayedAccountName = preferences.lastDisplayedAccountName.orEmpty() albumOperationListener = AlbumOperationListener(this) folderRefreshScheduler = FolderRefreshScheduler(this) intent?.let { - handleCommonIntents(it) - handleEcosystemIntent(it) + handleOpenFileIntents(it) + handleEcosystemAccountIntent(it) } - loadSavedInstanceState(savedInstanceState) - - // USER INTERFACE - initLayout() - initUI() - initTaskRetainerFragment() + restoreInstanceState(savedInstanceState) + inflateLayout() + setupViews() + addTaskRetainerFragmentIfMissing() if (intent.getBooleanExtra(EXTRA_RETURN_TO_PREVIEW, false)) { NavigationAnimator(this).prepareSlideUpEnter(savedInstanceState) { detailsHeaderImage() } } - // Restoring after UI has been inflated. + // needs the inflated layout savedInstanceState?.let { showSortListGroup(it.getBoolean(KEY_IS_SORT_GROUP_VISIBLE)) } - checkStoragePath() - observeWorkerState() - handleBackPress() + resetInvalidStoragePath() + observeWorkers() + registerBackPressCallback() setupDrawer(menuItemId) } - /** - * Determines which navigation drawer item should be selected. - * - * Resolution order: - * 1) Global app state (static flags in MainApp nav_personal and nav_on_device) - * 2) Currently visible fragment (and its active child) - * 3) Fallback to All Files - */ - override fun getMenuItemId(): Int = MainApp.getMenuItemId() ?: listOfFilesFragment?.menuItemId ?: R.id.nav_all_files + override fun onPostCreate(savedInstanceState: Bundle?) { + super.onPostCreate(savedInstanceState) - private fun loadSavedInstanceState(savedInstanceState: Bundle?) { - if (savedInstanceState == null) { - waitingToPreview = null - syncState = EmptyListState.LOADING - waitingToSend = null - return + // the storage permission is requested once the notification permission is answered + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + requestNotificationPermission(this) + } else { + requestStoragePermissionIfNeeded(this) } - savedInstanceState.run { - waitingToPreview = getParcelableArgument(KEY_WAITING_TO_PREVIEW, OCFile::class.java) - syncState = getParcelableArgument(KEY_SYNC_STATE, Parcelable::class.java) ?: EmptyListState.LOADING - waitingToSend = getParcelableArgument(KEY_WAITING_TO_SEND, OCFile::class.java) - searchQuery = getString(KEY_SEARCH_QUERY) - searchOpen = getBoolean(KEY_IS_SEARCH_OPEN, false) + if (savedInstanceState == null) { + addInitialListOfFilesFragment() + + if (intent.getBooleanExtra(EXTRA_RETURN_TO_PREVIEW, false)) { + handleNavigationIntent(intent) + } } - } - private fun initLayout() { - binding = FilesBinding.inflate(layoutInflater) - setContentView(binding.root) + migrateLegacyInstantUpload() + showOutdatedServerWarningIfNeeded() + updateNotificationButtonVisibility() } - private fun initUI() { - setupHomeSearchToolbarWithSortAndListButtons() - mMenuButton.setOnClickListener { openDrawer() } - findViewById(R.id.toolbar).setOnClickListener { openCurrentFolderActions() } - mSwitchAccountButton.setOnClickListener { showManageAccountsDialog() } - mNotificationButton.setOnClickListener { pushFragment(NavigatorScreen.Notifications) } - fastScrollUtils.fixAppBarForFastScroll(binding.appbar.appbar, binding.rootLayout) + public override fun onStart() { + super.onStart() - // reset ui states when file display activity created/recrated - listOfFilesFragment?.resetSearchAttributes() - } + registerReceivers() - private fun initTaskRetainerFragment() { - // Init Fragment without UI to retain AsyncTask across configuration changes - val tag = TaskRetainerFragment.FTAG_TASK_RETAINER_FRAGMENT - if (supportFragmentManager.findFragmentByTag(tag) is TaskRetainerFragment) return + if (SettingsActivity.isBackPressed) { + Log_OC.d(TAG, "User returned from settings activity, skipping reset content logic") + return + } - supportFragmentManager.beginTransaction().add(TaskRetainerFragment(), tag).commit() + initCurrentFileAndAccount() } - private fun checkStoragePath() { - val newStorage = Environment.getExternalStorageDirectory().absolutePath - val storagePath = preferences.getStoragePath(newStorage) - if (preferences.isStoragePathValid() || File(storagePath).exists()) return + override fun onResume() { + Log_OC.v(TAG, "onResume() start") - // falling back to default - preferences.setStoragePath(newStorage) - preferences.setStoragePathValid() - MainApp.setStoragePath(newStorage) - showWrongStoragePathDialog() + super.onResume() + + val listFragmentJustCreated = this.listFragmentJustCreated + this.listFragmentJustCreated = false + + folderRefreshScheduler.start() + + restoreSearchStateIfNeeded() + highlightCurrentNavigationItem() + + if (SettingsActivity.isBackPressed) { + Log_OC.d(TAG, "User returned from settings activity, skipping reset content logic") + return + } + + isFileDisplayActivityResumed = true + + val currentLeftFragment = leftFragment + if (currentLeftFragment !is OCFileListFragment) { + if (currentLeftFragment is FileFragment) { + super.updateActionBarTitleAndHomeButton(currentLeftFragment.file) + } + return + } + + if (isToolbarStyleSearch) { + setupHomeSearchToolbarWithSortAndListButtons() + } + syncCurrentFolder(ignoreETag = true, ignoreFocus = true) + refreshFileListOnResume(currentLeftFragment, listFragmentJustCreated) + + inAppReviewHelper.showInAppReview(this) + updateNotificationButtonVisibility() + + Log_OC.v(TAG, "onResume() end") + + Handler(Looper.getMainLooper()).postDelayed({ + isFileDisplayActivityResumed = false + }, ON_RESUMED_RESET_DELAY) } - private fun showWrongStoragePathDialog() { - try { - val builder = MaterialAlertDialogBuilder(this, R.style.Theme_ownCloud_Dialog) - .setTitle(R.string.wrong_storage_path) - .setMessage(R.string.wrong_storage_path_desc) - .setPositiveButton(R.string.dialog_close) { dialog, _ -> dialog?.dismiss() } - .setIcon(R.drawable.ic_settings) + override fun onRestart() { + super.onRestart() + checkForNewDevVersionNecessary(applicationContext) + } - viewThemeUtils.dialog.colorMaterialAlertDialogBackground(applicationContext, builder) + override fun onStop() { + Log_OC.v(TAG, "onStop()") + folderRefreshScheduler.stop() + unregisterReceivers() + super.onStop() + } - builder.create().show() - } catch (e: BadTokenException) { - Log_OC.e(TAG, "Error showing wrong storage info, so skipping it: ${e.message}") + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.run { + putParcelable(KEY_WAITING_TO_PREVIEW, waitingToPreview) + putParcelable(KEY_SYNC_STATE, syncState) + putParcelable(KEY_WAITING_TO_SEND, waitingToSend) + searchView?.let { putBoolean(KEY_IS_SEARCH_OPEN, !it.isIconified) } + putString(KEY_SEARCH_QUERY, searchQuery) + putBoolean(KEY_IS_SORT_GROUP_VISIBLE, sortListGroupVisibility()) } } @@ -418,52 +409,76 @@ class FileDisplayActivity : requestStoragePermissionIfNeeded(this) } - override fun onPostCreate(savedInstanceState: Bundle?) { - super.onPostCreate(savedInstanceState) + override fun onWindowFocusChanged(hasFocus: Boolean) { + super.onWindowFocusChanged(hasFocus) + if (!hasFocus) return - // handle notification permission on API level >= 33 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - // request notification permission first and then prompt for storage permissions - // storage permissions handled in onRequestPermissionsResult - requestNotificationPermission(this) - } else { - requestStoragePermissionIfNeeded(this) + pendingSyncFolderOperation?.let { + pendingSyncFolderOperation = null + it.run() } + } + // endregion + // region Setup + private fun restoreInstanceState(savedInstanceState: Bundle?) { if (savedInstanceState == null) { - addInitialListOfFilesFragment() + waitingToPreview = null + syncState = EmptyListState.LOADING + waitingToSend = null + return } - if (savedInstanceState == null && intent.getBooleanExtra(EXTRA_RETURN_TO_PREVIEW, false)) { - handleSpecialIntents(intent) + savedInstanceState.run { + waitingToPreview = getParcelableArgument(KEY_WAITING_TO_PREVIEW, OCFile::class.java) + syncState = getParcelableArgument(KEY_SYNC_STATE, Parcelable::class.java) ?: EmptyListState.LOADING + waitingToSend = getParcelableArgument(KEY_WAITING_TO_SEND, OCFile::class.java) + searchQuery = getString(KEY_SEARCH_QUERY, "") + wasSearchOpen = getBoolean(KEY_IS_SEARCH_OPEN, false) } + } - upgradeNotificationForInstantUpload() - checkOutdatedServer() - checkNotifications() + private fun inflateLayout() { + binding = FilesBinding.inflate(layoutInflater) + setContentView(binding.root) } - /** - * For Android 7+. Opens a pop up info for the new instant upload and disabled the old instant upload. - */ - private fun upgradeNotificationForInstantUpload() { - // check for Android 6+ if legacy instant upload is activated --> disable + show info + private fun setupViews() { + setupHomeSearchToolbarWithSortAndListButtons() + mMenuButton.setOnClickListener { openDrawer() } + findViewById(R.id.toolbar).setOnClickListener { openCurrentFolderActions() } + mSwitchAccountButton.setOnClickListener { showManageAccountsDialog() } + mNotificationButton.setOnClickListener { pushFragment(NavigatorScreen.Notifications) } + fastScrollUtils.fixAppBarForFastScroll(binding.appbar.appbar, binding.rootLayout) + listOfFilesFragment?.resetSearchAttributes() + } + + private fun addTaskRetainerFragmentIfMissing() { + val tag = TaskRetainerFragment.FTAG_TASK_RETAINER_FRAGMENT + if (supportFragmentManager.findFragmentByTag(tag) is TaskRetainerFragment) return + + supportFragmentManager.beginTransaction().add(TaskRetainerFragment(), tag).commit() + } + + private fun resetInvalidStoragePath() { + val defaultStoragePath = Environment.getExternalStorageDirectory().absolutePath + val storagePath = preferences.getStoragePath(defaultStoragePath) + if (preferences.isStoragePathValid() || File(storagePath).exists()) return + + preferences.setStoragePath(defaultStoragePath) + preferences.setStoragePathValid() + MainApp.setStoragePath(defaultStoragePath) + dialogs.showWrongStoragePath() + } + + private fun migrateLegacyInstantUpload() { if (!preferences.instantPictureUploadEnabled() && !preferences.instantVideoUploadEnabled()) return preferences.removeLegacyPreferences() - MaterialAlertDialogBuilder(this, R.style.Theme_ownCloud_Dialog) - .setTitle(R.string.drawer_synced_folders) - .setMessage(R.string.synced_folders_new_info) - .setPositiveButton(R.string.drawer_open) { dialog, _ -> - dialog?.dismiss() - pushFragment(NavigatorScreen.AutoUpload) - } - .setNegativeButton(R.string.drawer_close) { dialog, _ -> dialog?.dismiss() } - .setIcon(R.drawable.nav_synced_folders) - .show() + dialogs.showSyncedFoldersMigrationInfo { pushFragment(NavigatorScreen.AutoUpload) } } - private fun checkOutdatedServer() { + private fun showOutdatedServerWarningIfNeeded() { val currentUser = user.orElse(null) ?: return val capability = capabilities.orElse(null) ?: return @@ -478,7 +493,7 @@ class FileDisplayActivity : } } - private fun checkNotifications() { + private fun updateNotificationButtonVisibility() { lifecycleScope.launch { val client = clientRepository.getNextcloudClient() ?: run { Log_OC.e(TAG, "Could not fetch notifications!") @@ -493,26 +508,21 @@ class FileDisplayActivity : } } - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { - when (requestCode) { - // dialogue was dismissed -> prompt for storage permissions - PermissionUtil.PERMISSIONS_POST_NOTIFICATIONS -> requestStoragePermissionIfNeeded(this) - - PermissionUtil.PERMISSIONS_EXTERNAL_STORAGE -> if (grantResults.isPermissionGranted()) { - EventBus.getDefault().post(TokenPushEvent()) - } + private fun observeWorkers() { + lifecycleScope.launch { + viewModel.observeOfflineWorker(onComplete = { refreshCurrentDirectory() }) + } - PermissionUtil.PERMISSIONS_CAMERA -> if (grantResults.isPermissionGranted()) { - getOCFileListFragmentFromFile { it.directCameraUpload() } + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.observeFolderDownloadWorker( + onFileProgress = ::onFolderFileDownloadProgress, + onCompleted = ::updateListOfFilesFragment + ) } - - else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults) } } - // If request is cancelled, result arrays are empty. - private fun IntArray.isPermissionGranted(): Boolean = firstOrNull() == PackageManager.PERMISSION_GRANTED - private fun addInitialListOfFilesFragment() { val searchEvent = intent.getParcelableArgument(OCFileListFragment.SEARCH_EVENT, SearchEvent::class.java) val listOfFiles = OCFileListFragment().apply { @@ -531,39 +541,169 @@ class FileDisplayActivity : } } - private fun initFragments() { - val listOfFiles = listOfFilesFragment - if (listOfFiles != null && searchQuery.isNullOrEmpty()) { - if (!listFragmentJustCreated) { - listOfFiles.listDirectory(getCurrentDir(), file, MainApp.isOnlyOnDevice()) - } - listFragmentJustCreated = true - } else { - Log_OC.e(TAG, "Still have a chance to lose the initialization of list fragment >(") + private fun initCurrentFileAndAccount() { + val currentUser = user.orElse(null) ?: run { + Log_OC.w(TAG, "user is not available, cannot init file") + return + } + + val currentStorageManager: FileDataStorageManager = storageManager ?: run { + Log_OC.w(TAG, "storage manager is null, cannot init file") + return + } + + val initialFile = resolveInitialFile(currentStorageManager) ?: run { + Log_OC.e(TAG, "Could not retrieve root folder – cannot continue") + return + } + + setFile(initialFile) + + val accountName = currentUser.accountName + mSwitchAccountButton.tag = accountName + + avatarGenerator.setAccountAvatar( + currentUser, + this, + resources.getDimension(R.dimen.nav_drawer_menu_avatar_radius), + mSwitchAccountButton + ) + + if (accountName != lastDisplayedAccountName) { + composeProcessTextAlias.configure() + Log_OC.d(TAG, "Initializing Fragments in onAccountChanged..") + refreshListOfFilesAfterAccountChange() + if (initialFile.isFolder && searchQuery.isEmpty()) { + startSyncFolderOperation(initialFile, false) + } + } else { + updateActionBarTitleAndHomeButton(if (initialFile.isFolder) null else initialFile) + } + + saveLastDisplayedAccountName(accountName) + EventBus.getDefault().post(TokenPushEvent()) + checkForNewDevVersionNecessary(applicationContext) + } + + private fun resolveInitialFile(storageManager: FileDataStorageManager): OCFile? = + getFile()?.let { findStoredFile(it, storageManager) } ?: storageManager.getFileByPath(OCFile.ROOT_PATH) + + private fun findStoredFile(file: OCFile, storageManager: FileDataStorageManager): OCFile? { + if (!file.isDown || file.lastSyncDateForProperties != 0L) { + return storageManager.getFileByPath(file.remotePath) + } + + val parentPathLength = file.remotePath.lastIndexOf(file.fileName) + if (parentPathLength <= 0) return null + + val parentPath = file.remotePath.take(parentPathLength) + return file.takeIf { storageManager.getFileByPath(parentPath) != null } + } + + private fun saveLastDisplayedAccountName(accountName: String) { + preferences.lastDisplayedAccountName = accountName + lastDisplayedAccountName = accountName + } + + private fun refreshListOfFilesAfterAccountChange() { + val listOfFiles = listOfFilesFragment + if (listOfFiles != null && searchQuery.isEmpty()) { + if (!listFragmentJustCreated) { + listOfFiles.listDirectory(getCurrentDir(), file, MainApp.isOnlyOnDevice()) + } + listFragmentJustCreated = true + } else { + Log_OC.e(TAG, "Still have a chance to lose the initialization of list fragment >(") } resetScrollingAndUpdateActionBar() } - // region Handle Intents + private fun restoreSearchStateIfNeeded() { + val fragment = ocFileListFragment ?: return + if (fragment.isSearchFragment) { + fragment.loadSearchState(fragment.arguments) + } + } + + private fun highlightCurrentNavigationItem() { + val itemId = if (isAlbumsFragment || isAlbumItemsFragment) R.id.nav_album else menuItemId + highlightNavigationViewItem(itemId) + } + + private fun refreshFileListOnResume(fragment: OCFileListFragment, listFragmentJustCreated: Boolean) { + val startFile = getFileFromIntent(intent)?.also { file = it } + + val currentSearchView = searchView + when { + currentSearchView != null && searchQuery.isNotEmpty() -> currentSearchView.setQuery(searchQuery, false) + + !fragment.isSearchFragment && startFile == null -> { + if (!listFragmentJustCreated) { + fragment.listDirectory(MainApp.isOnlyOnDevice()) + } + fragment.registerFabListener() + updateActionBarTitleAndHomeButton(currentDir) + } + + else -> { + fragment.listDirectory(startFile, false) + updateActionBarTitleAndHomeButton(startFile) + } + } + } + // endregion + + // region Permissions + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + when (requestCode) { + PermissionUtil.PERMISSIONS_POST_NOTIFICATIONS -> requestStoragePermissionIfNeeded(this) + + PermissionUtil.PERMISSIONS_EXTERNAL_STORAGE -> if (grantResults.isPermissionGranted()) { + EventBus.getDefault().post(TokenPushEvent()) + } + + PermissionUtil.PERMISSIONS_CAMERA -> if (grantResults.isPermissionGranted()) { + withOCFileListFragment { it.directCameraUpload() } + } + + else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults) + } + } + + // If request is cancelled, result arrays are empty. + private fun IntArray.isPermissionGranted(): Boolean = firstOrNull() == PackageManager.PERMISSION_GRANTED + // endregion + + // region Intents @SuppressLint("UnsafeIntentLaunch") override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) setIntent(intent) - handleCommonIntents(intent) - handleSpecialIntents(intent) - handleRestartIntent(intent) - handleEcosystemIntent(intent) + handleOpenFileIntents(intent) + handleNavigationIntent(intent) + restartIfRequested(intent) + handleEcosystemAccountIntent(intent) + } + + @SuppressLint("UnsafeIntentLaunch") + private fun handleOpenFileIntents(intent: Intent) { + if (passCodeManager.isPassCodeEnabled() && passCodeManager.isLocked(this)) return + + when (intent.action) { + Intent.ACTION_VIEW -> handleViewIntent(intent) + OPEN_FILE -> openFileFromIntent(intent) + } } - private fun handleSpecialIntents(intent: Intent) { + private fun handleNavigationIntent(intent: Intent) { val action = intent.action when { ACTION_DETAILS.equals(action, ignoreCase = true) -> { - val file = getFileFromIntent(intent) - setFile(file) - showDetails(file) + val detailsFile = getFileFromIntent(intent) + setFile(detailsFile) + showDetails(detailsFile) isShowingDetailsFromPreview = intent.getBooleanExtra(EXTRA_RETURN_TO_PREVIEW, false) } @@ -573,7 +713,7 @@ class FileDisplayActivity : LIST_GROUPFOLDERS == action -> { Log_OC.d(this, "Switch to list groupfolders fragment") - leftFragment = GroupfolderListFragment() + replaceLeftFragment(GroupfolderListFragment()) supportFragmentManager.executePendingTransactions() } @@ -584,7 +724,7 @@ class FileDisplayActivity : } ON_DEVICE == action -> { - refreshOrInitOCFileListFragment() + showOrRefreshFileList() listOfFilesFragment?.currentSearchType = SearchType.ON_DEVICE updateActionBarTitleAndHomeButton(null) } @@ -594,37 +734,24 @@ class FileDisplayActivity : private fun switchToAllFiles() { Log_OC.d(this, "Switch to oc file fragment") - // Replace only if the fragment is NOT exactly OCFileListFragment - // Using `is OCFileListFragment` would also match subclasses, - // its needed because reinitializing OCFileListFragment itself causes an empty screen + // Only an exact OCFileListFragment is kept: re-creating it shows an empty screen, while its subclasses + // must be replaced. Album screens use their own tags, so leftFragment can still be the detached + // OCFileListFragment while an album screen is on top. val currentLeftFragment = leftFragment - // Album screens are committed under their own tags, so leftFragment can still - // point at the detached OCFileListFragment while an album screen is on top if (currentLeftFragment != null && (currentLeftFragment::class != OCFileListFragment::class || isAlbumsFragment || isAlbumItemsFragment) ) { - leftFragment = OCFileListFragment() + replaceLeftFragment(OCFileListFragment()) supportFragmentManager.executePendingTransactions() listFragmentJustCreated = true } - // The onResume() that always follows this same-activity intent redelivery already - // lists and re-syncs the current directory, so doing it again here is redundant. + // the onResume() following this intent already lists and re-syncs the current directory browseToRoot(performRefresh = false) } @SuppressLint("UnsafeIntentLaunch") - private fun handleCommonIntents(intent: Intent) { - if (passCodeManager.isPassCodeEnabled() && passCodeManager.isLocked(this)) return - - when (intent.action) { - Intent.ACTION_VIEW -> handleOpenFileViaIntent(intent) - OPEN_FILE -> onOpenFileIntent(intent) - } - } - - @SuppressLint("UnsafeIntentLaunch") - private fun handleRestartIntent(intent: Intent) { + private fun restartIfRequested(intent: Intent) { if (intent.action != RESTART) return finish() @@ -632,10 +759,8 @@ class FileDisplayActivity : } private fun handleSearchIntent(intent: Intent) { - val searchEvent = intent.getParcelableArgument( - OCFileListFragment.SEARCH_EVENT, - SearchEvent::class.java - ) ?: return + val searchEvent = intent.getParcelableArgument(OCFileListFragment.SEARCH_EVENT, SearchEvent::class.java) + ?: return val searchFragment = when (searchEvent.searchType) { SearchRemoteOperation.SearchType.PHOTO_SEARCH -> { @@ -653,1160 +778,295 @@ class FileDisplayActivity : OCFileListFragment() } } - leftFragment = searchFragment.apply { arguments = searchEvent.getBundle() } + replaceLeftFragment(searchFragment.apply { arguments = searchEvent.getBundle() }) listOfFilesFragment?.run { isSearchFragment = true updateSearchType(searchEvent) } } - // endregion - private fun onOpenFileIntent(intent: Intent) { - val file = getFileFromIntent(intent) ?: run { + private fun openFileFromIntent(intent: Intent) { + val fileToOpen = getFileFromIntent(intent) ?: run { Log_OC.e(TAG, "Can't open file intent, file is null") return } - // Ensure we have the correct fragment type - if (leftFragment !is OCFileListFragment || leftFragment is GalleryFragment) { - Log_OC.w( - TAG, - "Invalid fragment (${leftFragment?.let { it::class.simpleName } ?: "null"}). " + - "Replacing." - ) - setLeftFragment(OCFileListFragment(), false) + val currentLeftFragment = leftFragment + if (currentLeftFragment !is OCFileListFragment || currentLeftFragment is GalleryFragment) { + val fragmentName = currentLeftFragment?.let { it::class.simpleName } ?: "null" + Log_OC.w(TAG, "Invalid fragment ($fragmentName). Replacing.") + replaceLeftFragment(OCFileListFragment(), showSortListGroup = false) } - // Ensure fragment is attached before interaction + // waits until the fragment is attached Handler(Looper.getMainLooper()).post { - (supportFragmentManager.findFragmentByTag(TAG_LIST_OF_FILES) as? OCFileListFragment)?.let { fragment -> - leftFragment = fragment - fragment.setFileDepth(file) - updateActionBarTitleAndHomeButton(file) - fragment.onItemClicked(file) - } + val fragment = supportFragmentManager.findFragmentByTag(TAG_LIST_OF_FILES) as? OCFileListFragment + ?: return@post + replaceLeftFragment(fragment) + fragment.setFileDepth(fileToOpen) + updateActionBarTitleAndHomeButton(fileToOpen) + fragment.onItemClicked(fileToOpen) } } - private fun setLeftFragment(fragment: Fragment?, showSortListGroup: Boolean) { - fragment ?: return - - isShowingDetailsFromPreview = false - prepareFragmentBeforeCommit(showSortListGroup) - commitFragment(fragment) - } + private fun getFileFromIntent(intent: Intent?): OCFile? = + intent.getParcelableArgument(EXTRA_FILE, OCFile::class.java) + ?: intent?.getStringExtra(EXTRA_FILE_REMOTE_PATH) + ?.let { fileDataStorageManager.getFileByDecryptedRemotePath(it) } - private fun prepareFragmentBeforeCommit(showSortListGroup: Boolean) { - searchView?.post { searchView?.setQuery(searchQuery, true) } - setDrawerIndicatorEnabled(false) + private fun handleEcosystemAccountIntent(intent: Intent) { + ecosystemManager.receiveAccount( + intent, + object : AccountReceiverCallback { + override fun onAccountReceived(accountName: String) { + val receivedUser = accountManager.getUser(accountName).orElse(null) ?: run { + Log_OC.w(TAG, "user is not present") + SnackbarUtil.show(this@FileDisplayActivity, R.string.account_not_found) + return + } - // clear the subtitle while navigating to any other screen from Media screen - clearToolbarSubtitle() + accountClicked(receivedUser) + } - showSortListGroup(showSortListGroup) + override fun onAccountError(reason: String) { + Log_OC.w(TAG, "handleEcosystemIntent: $reason") + } + } + ) } + // endregion - private fun commitFragment(fragment: Fragment): Boolean { - val fragmentManager = supportFragmentManager - if (!isActive() || fragmentManager.isDestroyed || fragmentManager.isStateSaved) { - Log_OC.d(TAG, "${fragment.javaClass.simpleName} not committed, skipping") - return false - } - - fragmentManager.beginTransaction() - .addToBackStack(null) - .replace(R.id.left_fragment_container, fragment, TAG_LIST_OF_FILES) - .commit() + // region Open remote file + private fun handleViewIntent(intent: Intent) { + SnackbarUtil.show(this, getString(R.string.retrieving_file)) - Log_OC.d(TAG, "${fragment.javaClass.simpleName} committed, pending transaction") + val accountName = intent.getStringExtra(KEY_ACCOUNT) + val fileId = intent.getStringExtra(KEY_FILE_ID) + val filePath = intent.getStringExtra(KEY_FILE_PATH) - return true - } + val intentData = intent.data + if (accountName == null && fileId == null && intentData != null) { + openDeepLink(intentData) + return + } - private fun getOCFileListFragmentFromFile(onFragmentReady: (OCFileListFragment) -> Unit) { - val currentLeftFragment = leftFragment - if (currentLeftFragment is OCFileListFragment) { - onFragmentReady(currentLeftFragment) + val optionalUser = if (accountName == null) user else userAccountManager.getUser(accountName) + val targetUser = optionalUser.orElse(null) ?: run { + SnackbarUtil.show(this, getString(R.string.associated_account_not_found)) return } - val listOfFiles = OCFileListFragment().apply { - arguments = Bundle().apply { putBoolean(OCFileListFragment.ARG_ALLOW_CONTEXTUAL_ACTIONS, true) } + when { + !fileId.isNullOrEmpty() -> openFileById(targetUser, fileId) + !filePath.isNullOrEmpty() -> openFileByPath(targetUser, filePath) + else -> accountClicked(targetUser) } + } - runOnUiThread { - val fragmentManager = supportFragmentManager - if (fragmentManager.isStateSaved || fragmentManager.isDestroyed) return@runOnUiThread + private fun openDeepLink(uri: Uri) { + val match = DeepLinkHandler(userAccountManager).parseDeepLink(uri) - prepareFragmentBeforeCommit(true) - if (commitFragment(listOfFiles)) { - fragmentManager.executePendingTransactions() - onFragmentReady(listOfFiles) - } + when { + match == null -> handleDeepLink(uri) + match.users.isEmpty() -> SnackbarUtil.show(this, getString(R.string.associated_account_not_found)) + match.users.size == SINGLE_USER_SIZE -> openFileById(match.users.first(), match.fileId) + else -> chooseUserAndOpenFile(match.users, match.fileId) } } - fun showFileActions(file: OCFile?) { + private fun chooseUserAndOpenFile(users: List, fileId: String) { dismissLoadingDialog() - getOCFileListFragmentFromFile { fragment -> - browseUp(fragment) - file?.let { fragment.onOverflowIconClicked(it, null) } + dialogs.showAccountChooser(users) { chosenUser -> + openFileById(chosenUser, fileId) + showLoadingDialog(getString(R.string.retrieving_file)) } } - var leftFragment: Fragment? - get() = supportFragmentManager.findFragmentByTag(TAG_LIST_OF_FILES) - - // Replaces the first fragment managed by the activity with the received as a parameter. - private set(fragment) { - setLeftFragment(fragment, true) - } + private fun openFileById(user: User, fileId: String) { + setUser(user) - val listOfFilesFragment: OCFileListFragment? - get() = supportFragmentManager.findFragmentByTag(TAG_LIST_OF_FILES) as? OCFileListFragment ?: run { - Log_OC.e(TAG, "Access to unexisting list of files fragment") - null + val capability = capabilities.orElse(null) ?: run { + Log_OC.e(TAG, "cannot fetch remote file, capabilities is empty") + return } - protected fun resetScrollingAndUpdateActionBar() { - updateActionBarTitleAndHomeButton(file) - resetScrolling(true) + val fileStorageManager = storageManager ?: FileDataStorageManager(user, contentResolver) + val weakContext = WeakReference(this) + val task = FetchRemoteFileTask(user, fileId, fileStorageManager, lifecycleScope, capability, weakContext) + task.run(showFile = { (fetchedFile, message) -> + fetchedFile?.let { file = it } + showFile(fetchedFile, message) + }) } - fun updateListOfFilesFragment() { - listOfFilesFragment?.listDirectory(MainApp.isOnlyOnDevice()) + private fun openFileByPath(user: User, filePath: String) { + setUser(user) + + val fileStorageManager = storageManager ?: FileDataStorageManager(user, contentResolver) + lifecycleScope.launch { + val client = clientRepository.getOwncloudClient() ?: run { + onFileRequestError(null) + return@launch + } + + val task = GetRemoteFileTask(this@FileDisplayActivity, filePath, client, fileStorageManager, user) + asyncRunner.postQuickTask( + task, + { result: GetRemoteFileTask.Result -> onFileRequestResult(result) }, + { throwable: Throwable? -> onFileRequestError(throwable) } + ) + } } - fun resetSearchView() { - listOfFilesFragment?.isSearchFragment = false + private fun onFileRequestError(throwable: Throwable?) { + dismissLoadingDialog() + SnackbarUtil.show(this, getString(R.string.error_retrieving_file)) + Log_OC.e(TAG, "Requesting file from remote failed!", throwable) } - protected fun refreshDetailsFragmentIfVisible( - downloadEvent: String, - downloadedRemotePath: String, - success: Boolean - ) { - val detailFragment = leftFragment as? FileDetailFragment ?: return - val fileInFragment = detailFragment.file + private fun onFileRequestResult(result: GetRemoteFileTask.Result) { + dismissLoadingDialog() - if (fileInFragment != null && downloadedRemotePath != fileInFragment.remotePath) { - waitingToPreview = null - return - } + file = result.file - when (downloadEvent) { - FileDownloadEventBroadcaster.ACTION_DOWNLOAD_ENQUEUED -> { - detailFragment.listenForTransferProgress() - detailFragment.updateFileDetails(true, false) - } - - FileDownloadEventBroadcaster.ACTION_DOWNLOAD_COMPLETED -> { - val waitedPreview = waitingToPreview?.remotePath == downloadedRemotePath - val previewStarted = waitedPreview && tryStartWaitingPreview(success) - waitingToPreview = null - if (!previewStarted) { - detailFragment.updateFileDetails(false, success) - } - } - } - } - - fun canPreviewInAudioPlayer(file: OCFile?): Boolean = file != null && MimeTypeUtil.isAudio(file) - - private fun tryStartWaitingPreview(success: Boolean): Boolean { - if (!success) return false - - waitingToPreview = waitingToPreview?.let { storageManager.getFileById(it.fileId) } - val file = waitingToPreview ?: return false - - return when { - MimeTypeUtil.isVideo(file) -> { - startImagePreview(file, true) - true - } - - canPreviewInAudioPlayer(file) -> { - startAudioPreview(file, true, true) - true - } - - MimeTypeUtil.isVCard(file.mimeType) -> { - startContactListFragment(file) - true - } - - PreviewTextFileFragment.canBePreviewed(file) -> { - startTextPreview(file, true) - true - } - - MimeTypeUtil.isPDF(file) -> { - startPdfPreview(file) - true - } - - else -> { - fileOperationsHelper.openFile(file) - false - } - } - } - - override fun onPrepareOptionsMenu(menu: Menu?): Boolean { - val drawerOpen = isDrawerOpen - drawerMenuItemsToShowHide.forEach { it.isVisible = !drawerOpen } - - menu?.findItem(R.id.action_current_folder_actions)?.isVisible = canShowCurrentFolderActions() - - return super.onPrepareOptionsMenu(menu) - } - - private fun canShowCurrentFolderActions(): Boolean = leftFragment is OCFileListFragment && - !isDrawerOpen && - !isSearchOpen() && - !isRoot(getCurrentDir()) && - listOfFilesFragment?.isSearchFragment == false && - !isAlbumItemsFragment - - private fun openCurrentFolderActions() { - if (canShowCurrentFolderActions()) { - listOfFilesFragment?.openCurrentFolderActionsMenu() - } - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.activity_file_display, menu) - - menu.findItem(R.id.action_select_all).isVisible = false - val searchMenuItem = menu.findItem(R.id.action_search) - searchView = searchMenuItem.actionView as? SearchView - searchMenuItem.isVisible = false - mSearchText.setOnClickListener { - showSearchView() - searchView?.postDelayed({ - searchView?.isIconified = false - searchView?.requestFocusFromTouch() - }, SEARCH_VIEW_FOCUS_DELAY) - } - - // populate list of menu items to show/hide when drawer is opened/closed - drawerMenuItemsToShowHide = listOf(searchMenuItem) - - searchView?.let { setupSearchView(it) } - - return super.onCreateOptionsMenu(menu) - } - - private fun setupSearchView(view: SearchView) { - viewThemeUtils.androidx.themeToolbarSearchView(view) - - if (!searchQuery.isNullOrEmpty()) { - view.post { restoreSearchQuery() } - } - - view.setOnCloseListener { - onSearchViewClosed(view) - true - } - - view.findViewById(androidx.appcompat.R.id.search_edit_frame)?.let { - disableDrawerIndicatorWhenVisible(it) - } - } - - private fun restoreSearchQuery() { - searchView?.run { - isIconified = false - setQuery(searchQuery, true) - clearFocus() - } - } - - private fun onSearchViewClosed(view: SearchView) { - if (!view.query.isNullOrEmpty()) { - view.post { searchView?.setQuery("", true) } - return - } - - view.onActionViewCollapsed() - setDrawerIndicatorEnabled(isDrawerIndicatorAvailable) // order matters - supportActionBar?.setDisplayHomeAsUpEnabled(true) - mDrawerToggle.syncState() - - listOfFilesFragment?.run { - isSearchFragment = false - refreshDirectory() - } - } - - private fun disableDrawerIndicatorWhenVisible(searchEditFrame: View) { - searchEditFrame.viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener { - var oldVisibility: Int = UNKNOWN_VISIBILITY - - override fun onGlobalLayout() { - val currentVisibility = searchEditFrame.visibility - if (currentVisibility == oldVisibility) return - - if (currentVisibility == View.VISIBLE) { - setDrawerIndicatorEnabled(false) - } - oldVisibility = currentVisibility - } - }) - } - - private fun shouldOpenDrawer(): Boolean = !isDrawerOpen && - !isSearchOpen() && - isRoot(getCurrentDir()) && - leftFragment is OCFileListFragment && - !isAlbumItemsFragment - - /** - * Called, when the user selected something for uploading - */ - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - when { - data != null && requestCode in selectContentFromAppsRequestCodes && - resultCode in selectContentFromAppsResultCodes -> - requestUploadOfContentFromApps(requestCode, resultCode, data) - - data != null && requestCode == REQUEST_CODE__SELECT_FILES_FROM_FILE_SYSTEM && - resultCode in selectFilesFromFileSystemResultCodes -> - requestUploadOfFilesFromFileSystem(data, resultCode) - - requestCode in uploadFromCameraRequestCodes && resultCode in uploadFromCameraResultCodes -> - uploadFromCamera(requestCode) - - requestCode == REQUEST_CODE__MOVE_OR_COPY_FILES && resultCode == RESULT_OK -> exitSelectionMode() - - else -> super.onActivityResult(requestCode, resultCode, data) - } - } - - private fun uploadFromCamera(requestCode: Int) { - val isVideo = requestCode == REQUEST_CODE__UPLOAD_FROM_VIDEO_CAMERA - val cameraFilePath = FileOperationsHelper.createCameraFile(this, isVideo).absolutePath - - val listener = object : CheckAvailableSpaceListener { - override fun onCheckAvailableSpaceStart() { - Log_OC.d(this, "onCheckAvailableSpaceStart") - } - - override fun onCheckAvailableSpaceFinish(hasEnoughSpaceAvailable: Boolean, vararg filesToUpload: String?) { - Log_OC.d(this, "onCheckAvailableSpaceFinish") - if (!hasEnoughSpaceAvailable) return - - val capturedFilePath = filesToUpload.firstOrNull() ?: return - renameAndUploadCapturedFile(File(capturedFilePath), requestCode) - } - } - - CheckAvailableSpaceTask(listener, cameraFilePath).execute() - } - - private fun renameAndUploadCapturedFile(capturedFile: File, requestCode: Int) { - val capturedFileName = if (requestCode == REQUEST_CODE__UPLOAD_FROM_CAMERA) { - FileOperationsHelper.getCapturedImageName() - } else { - FileOperationsHelper.getCapturedVideoName() - } - val renamedFile = File(capturedFile.parent + OCFile.PATH_SEPARATOR + capturedFileName) - - if (!capturedFile.renameTo(renamedFile)) { - SnackbarUtil.show(this, R.string.error_uploading_direct_camera_upload) - return - } - - requestUploadOfFilesFromFileSystem( - renamedFile.parentFile?.absolutePath, - arrayOf(renamedFile.absolutePath), - FileUploadWorker.LOCAL_BEHAVIOUR_DELETE - ) - } - - private fun exitSelectionMode() { - listOfFilesFragment?.exitSelectionMode() - } - - private fun requestUploadOfFilesFromFileSystem(data: Intent, resultCode: Int) { - val filePaths = data.getStringArrayExtra(UploadFilesActivity.EXTRA_CHOSEN_FILES) ?: return - val basePath = data.getStringExtra(UploadFilesActivity.LOCAL_BASE_PATH) - requestUploadOfFilesFromFileSystem(basePath, filePaths, resultCode) - } - - private fun getRemotePaths(directory: String?, filePaths: Array, localBasePath: String): Array = - filePaths.map { (directory ?: "") + StringUtils.removePrefix(it, localBasePath) }.toTypedArray() - - private fun requestUploadOfFilesFromFileSystem(localBasePath: String?, filePaths: Array, resultCode: Int) { - if (localBasePath == null) { - Log_OC.d(TAG, "User clicked on 'Update' with no selection") - SnackbarUtil.show(this, R.string.filedisplay_no_file_selected) - return - } - - val normalizedLocalBasePath = if (localBasePath.endsWith("/")) localBasePath else "$localBasePath/" - val remotePathBase = getCurrentDir()?.remotePath - val decryptedRemotePaths = getRemotePaths(remotePathBase, filePaths, normalizedLocalBasePath) - - val behaviour = when (resultCode) { - UploadFilesActivity.RESULT_OK_AND_MOVE -> FileUploadWorker.LOCAL_BEHAVIOUR_MOVE - UploadFilesActivity.RESULT_OK_AND_DELETE -> FileUploadWorker.LOCAL_BEHAVIOUR_DELETE - else -> FileUploadWorker.LOCAL_BEHAVIOUR_FORGET - } - - connectivityService.isNetworkAndServerAvailable { isAvailable: Boolean? -> - if (isAvailable == true) { - uploadNewFiles(remotePathBase, filePaths, decryptedRemotePaths, behaviour) - } else { - addCreateFileOfflineOperation(filePaths, decryptedRemotePaths) - } - } - } - - private fun uploadNewFiles( - remotePathBase: String?, - filePaths: Array, - decryptedRemotePaths: Array, - behaviour: Int - ) { - val capability = capabilities.orElse(null) ?: return - - val isValidFolderPath = remotePathBase?.let { checkFolderPath(it, capability, this) } - if (isValidFolderPath == false) { - SnackbarUtil.show(this, R.string.file_name_validator_error_contains_reserved_names_or_invalid_characters) - return - } - - val currentUser = user.orElse(null) ?: return - val data = OCUploadLocalPathData.forFile( - currentUser, - filePaths, - decryptedRemotePaths, - behaviour, - createRemoteFolder = true - ) - - FileUploadHelper.instance().uploadNewFiles(data) - } - - private fun addCreateFileOfflineOperation(filePaths: Array, decryptedRemotePaths: Array) { - lifecycleScope.launch { - withContext(Dispatchers.IO) { - fileDataStorageManager.addCreateFileOfflineOperation(filePaths, decryptedRemotePaths) - } - refreshCurrentDirectory() - } - } - - private fun requestUploadOfContentFromApps(requestCode: Int, resultCode: Int, contentIntent: Intent) { - val currentUser = user.orElse(null) ?: return - - val clipData = contentIntent.clipData - val streamsToUpload: List = if (clipData != null && clipData.itemCount > 0) { - List(clipData.itemCount) { clipData.getItemAt(it)?.uri } - } else { - listOf(contentIntent.data) - } - - val behaviour = if (resultCode == UploadFilesActivity.RESULT_OK_AND_MOVE) { - FileUploadWorker.LOCAL_BEHAVIOUR_MOVE - } else { - FileUploadWorker.LOCAL_BEHAVIOUR_COPY - } - - val remotePath = getCurrentDir()?.remotePath ?: OCFile.ROOT_PATH - val fileDisplayNameTransformer = if (requestCode == REQUEST_CODE__SELECT_CONTENT_FROM_APPS_AUTO_RENAME) { - Function { timestampedFileNameFor(it) } - } else { - null - } - - UriUploader( - this, - streamsToUpload, - remotePath, - currentUser, - behaviour, - false, // Not show waiting dialog while file is being copied from private storage - null, // Not needed copy temp task listener - fileDisplayNameTransformer - ).uploadUris() - } - - private fun timestampedFileNameFor(uri: Uri): String? { - val displayName = UriUtils.getDisplayNameForUri(uri, applicationContext) - if (displayName.isNullOrEmpty()) return null - - return FileOperationsHelper.getTimestampedFileName("." + FilenameUtils.getExtension(displayName)) - } - - fun isSearchOpen(): Boolean = - searchView?.findViewById(androidx.appcompat.R.id.search_edit_frame)?.isVisible == true - - /** - * Sets up a custom back-press handler for this activity. - * - * This callback determines how the back button behaves based on the current UI state: - * - If the search view is open, it closes it. - * - If the navigation drawer is open, it closes it. - * - If the left fragment is an [OCFileListFragment]: - * - If in the root directory, it either navigates to "All Files" or finishes the activity. - * - Otherwise, it navigates one level up. - * - Otherwise, it pops the current fragment from the back stack. - * - * ### About `isEnabled` - * `isEnabled` is a property of [OnBackPressedCallback]. - * When `isEnabled = false`, this callback is **temporarily disabled**, - * allowing the system or other callbacks to handle the back press instead. - */ - private fun handleBackPress() { - onBackPressedDispatcher.addCallback( - this, - object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - handleBackPressImpl(before = { - isEnabled = false - }, after = { - isEnabled = true - }) - } - } - ) - } - - private fun handleBackPressImpl(before: () -> Unit = {}, after: () -> Unit = {}) { - val currentLeftFragment = leftFragment - - before() - when { - isSearchOpen() -> resetSearchAction() - - isDrawerOpen -> closeDrawer() - - isShowingDetailsFromPreview && currentLeftFragment is FileDetailFragment -> - NavigationAnimator(this).finishWithSlideDown() - - // pop back if current fragment is AlbumItemsFragment - isAlbumItemsFragment -> popBack() - - currentLeftFragment is OCFileListFragment -> handleOCFileListFragmentBackPress(currentLeftFragment) - - else -> popBack() - } - after() - } - - private fun handleOCFileListFragmentBackPress(fragment: OCFileListFragment) { - // Normal folder navigation (go up) also works for shared tab - if (!isRoot(getCurrentDir())) { - browseUp(fragment) - return - } - - if (fragment.shouldNavigateBackToAllFiles()) navigateToAllFiles() else finish() - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { - android.R.id.home -> { - if (shouldOpenDrawer()) openDrawer() else handleBackPressImpl() - true - } - - R.id.action_select_all -> { - listOfFilesFragment?.selectAllFiles(true) - true - } - - R.id.action_current_folder_actions -> { - openCurrentFolderActions() - true - } - - else -> super.onOptionsItemSelected(item) - } - - private fun browseUp(listOfFiles: OCFileListFragment) { - listOfFiles.onBrowseUp() - val currentFile = listOfFiles.currentFile - - file = currentFile - - currentFile?.let { - listOfFiles.setFabVisible(it.canCreateFileAndFolder()) - listOfFiles.registerFabListener() - } - - resetScrollingAndUpdateActionBar() - startMetadataSyncForCurrentDir() - } - - private fun resetSearchAction() { - val currentLeftFragment = leftFragment - val currentSearchView = searchView ?: return - if (!isSearchOpen()) return - - currentSearchView.run { - clearFocus() - val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager - inputMethodManager?.hideSoftInputFromWindow(windowToken, 0) - setQuery("", false) - onActionViewCollapsed() - } - - if (isRoot(getCurrentDir()) && currentLeftFragment is OCFileListFragment) { - // Remove the list to the original state - currentLeftFragment.adapter?.let { adapter -> - currentLeftFragment.performSearch("", adapter.listOfHiddenFiles, true) - } - hideSearchView(getCurrentDir()) - setDrawerIndicatorEnabled(isDrawerIndicatorAvailable) - } - - if (currentLeftFragment is UnifiedSearchFragment) { - showSortListGroup(false) - supportFragmentManager.popBackStack() - } - } - - /** - * Use this method when want to pop the fragment on back press. It resets Scrolling (See - * [with true][.resetScrolling] and pop the visibility for sortListGroup (See - * [with false][.showSortListGroup]. At last call to supportFragmentManager.popBackStack() - */ - private fun popBack() { - binding.fabMain.setImageResource(R.drawable.ic_plus) - resetScrolling(true) - showSortListGroup(false) - showBottomNavigationBar(true) - supportFragmentManager.popBackStack() - } - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.run { - putParcelable(KEY_WAITING_TO_PREVIEW, waitingToPreview) - putParcelable(KEY_SYNC_STATE, syncState) - putParcelable(KEY_WAITING_TO_SEND, waitingToSend) - searchView?.let { putBoolean(KEY_IS_SEARCH_OPEN, !it.isIconified) } - putString(KEY_SEARCH_QUERY, searchQuery) - putBoolean(KEY_IS_SORT_GROUP_VISIBLE, sortListGroupVisibility()) - } - } - - override fun onResume() { - Log_OC.v(TAG, "onResume() start") - - super.onResume() - - val listFragmentJustCreated = this.listFragmentJustCreated - this.listFragmentJustCreated = false - - folderRefreshScheduler.start() - - restoreSearchStateIfNeeded() - highlightCurrentNavigationItem() - - if (SettingsActivity.isBackPressed) { - Log_OC.d(TAG, "User returned from settings activity, skipping reset content logic") - return - } - - isFileDisplayActivityResumed = true - - // Instead of onPostCreate, starting the loading in onResume for children fragments - val currentLeftFragment = leftFragment - if (currentLeftFragment !is OCFileListFragment) { - if (currentLeftFragment is FileFragment) { - super.updateActionBarTitleAndHomeButton(currentLeftFragment.file) - } - return - } - - if (isToolbarStyleSearch) { - setupHomeSearchToolbarWithSortAndListButtons() - } - syncAndUpdateFolder(ignoreETag = true, ignoreFocus = true) - refreshListOfFiles(currentLeftFragment, listFragmentJustCreated) - - inAppReviewHelper.showInAppReview(this) - checkNotifications() - - Log_OC.v(TAG, "onResume() end") - - Handler(Looper.getMainLooper()).postDelayed({ - isFileDisplayActivityResumed = false - }, ON_RESUMED_RESET_DELAY) - } - - private fun restoreSearchStateIfNeeded() { - val fragment = ocFileListFragment ?: return - if (fragment.isSearchFragment) { - fragment.loadSearchState(fragment.arguments) - } - } - - private fun highlightCurrentNavigationItem() { - val itemId = if (isAlbumsFragment || isAlbumItemsFragment) R.id.nav_album else menuItemId - highlightNavigationViewItem(itemId) - } - - private fun refreshListOfFiles(fragment: OCFileListFragment, listFragmentJustCreated: Boolean) { - // Try to get the OCFile from the intent, if one was provided when launching this activity. - // 'file' comes from the FileActivity base class and represents the currently opened file or folder. - // We update it only when a valid file is found in the intent. - val startFile = getFileFromIntent(intent)?.also { file = it } - - val currentSearchView = searchView - when { - currentSearchView != null && !searchQuery.isNullOrEmpty() -> currentSearchView.setQuery(searchQuery, false) - - !fragment.isSearchFragment && startFile == null -> { - if (!listFragmentJustCreated) { - fragment.listDirectory(MainApp.isOnlyOnDevice()) - } - fragment.registerFabListener() - updateActionBarTitleAndHomeButton(currentDir) - } - - else -> { - fragment.listDirectory(startFile, false) - updateActionBarTitleAndHomeButton(startFile) - } - } - } - - private fun getFileFromIntent(intent: Intent?): OCFile? = - intent.getParcelableArgument(EXTRA_FILE, OCFile::class.java) - ?: intent?.getStringExtra(EXTRA_FILE_REMOTE_PATH) - ?.let { fileDataStorageManager.getFileByDecryptedRemotePath(it) } - - // region local broadcast manager receivers - private fun registerReceivers() { - Log_OC.d(TAG, "registering receivers") - - localBroadcastManager.run { - val uploadFinishedIntent = IntentFilter(FileUploadEventBroadcaster.ACTION_UPLOAD_COMPLETED) - registerReceiver(fileUploadCompletedReceiver, uploadFinishedIntent) - - val fileDownloadStartedIntentFilter = IntentFilter(FileDownloadEventBroadcaster.ACTION_DOWNLOAD_ENQUEUED) - registerReceiver(fileDownloadStartedReceiver, fileDownloadStartedIntentFilter) - - val fileDownloadFinishedIntentFilter = IntentFilter(FileDownloadEventBroadcaster.ACTION_DOWNLOAD_COMPLETED) - registerReceiver(fileDownloadCompletedReceiver, fileDownloadFinishedIntentFilter) - - val syncBroadcastIntentFilter = IntentFilter(FileSyncAdapter.EVENT_FULL_SYNC_START).apply { - addAction(FileSyncAdapter.EVENT_FULL_SYNC_END) - addAction(FileSyncAdapter.EVENT_FULL_SYNC_FOLDER_CONTENTS_SYNCED) - addAction(RefreshFolderOperation.EVENT_SINGLE_FOLDER_CONTENTS_SYNCED) - addAction(RefreshFolderOperation.EVENT_SINGLE_FOLDER_SHARES_SYNCED) - } - registerReceiver(syncReceiver, syncBroadcastIntentFilter) - } - } - - private fun unregisterReceivers() { - Log_OC.d(TAG, "unregistering receivers") - - localBroadcastManager.run { - unregisterReceiver(syncReceiver) - unregisterReceiver(fileUploadCompletedReceiver) - unregisterReceiver(fileDownloadStartedReceiver) - unregisterReceiver(fileDownloadCompletedReceiver) - } - } - // endregion - - override fun onStop() { - Log_OC.v(TAG, "onStop()") - folderRefreshScheduler.stop() - unregisterReceivers() - super.onStop() - } - - override fun onSortingOrderChosen(selection: FileSortOrder?) { - listOfFilesFragment?.fileListLayoutManager?.sortFiles(selection) - } - - override fun downloadFile(file: OCFile, packageName: String, activityName: String) { - startDownloadForSending(file, OCFileListFragment.DOWNLOAD_SEND, packageName, activityName) - } - - // region SyncBroadcastReceiver - private inner class SyncReceiver : BroadcastReceiver() { - @SuppressLint("VisibleForTests") - override fun onReceive(context: Context?, intent: Intent) { - try { - val event = intent.action - Log_OC.d(TAG, "Received broadcast $event") - - // region EventData - val accountName = intent.getStringExtra(FileSyncAdapter.EXTRA_ACCOUNT_NAME) - val syncFolderRemotePath = intent.getStringExtra(FileSyncAdapter.EXTRA_FOLDER_PATH) - val id = intent.getStringExtra(FileSyncAdapter.EXTRA_RESULT) - val syncResult = DataHolderUtil.getInstance().retrieve(id) - val sameAccount = accountName != null && accountName == account?.name && storageManager != null - val fileListFragment = this@FileDisplayActivity.listOfFilesFragment - - // endregion - if (sameAccount) { - handleSyncEvent(event, syncFolderRemotePath, id, fileListFragment, syncResult) - } - - if (syncResult is RemoteOperationResult<*> && - syncResult.code == RemoteOperationResult.ResultCode.SSL_RECOVERABLE_PEER_UNVERIFIED - ) { - mLastSslUntrustedServerResult = syncResult - } - } catch (_: java.lang.RuntimeException) { - safelyDeleteResult(intent) - } finally { - onSyncFinished() - } - } - } - - // avoid app crashes after changing the serial id of RemoteOperationResult in owncloud library - // with broadcast notifications pending to process - private fun safelyDeleteResult(intent: Intent) { - try { - DataHolderUtil.getInstance().delete(intent.getStringExtra(FileSyncAdapter.EXTRA_RESULT)) - } catch (_: java.lang.RuntimeException) { - Log_OC.i(TAG, "Ignoring error deleting data") - } - } - - private fun handleSyncEvent( - event: String?, - syncFolderRemotePath: String?, - id: String?, - fileListFragment: OCFileListFragment?, - syncResult: Any? - ) { - if (FileSyncAdapter.EVENT_FULL_SYNC_START == event) { - return - } - - // EVENT_SINGLE_FOLDER_CONTENTS_SYNCED fires only when the folder's content actually changed, and - // EVENT_SINGLE_FOLDER_SHARES_SYNCED only when a sharee actually changed - each is an independent, - // already-precise signal, so both are handled here (RefreshFolderOperation.java). - var currentFile = file?.remotePath?.let { storageManager.getFileByPath(it) } - val currentDir = getCurrentDir()?.remotePath?.let { storageManager.getFileByPath(it) } - val isSyncFolderRemotePathRoot = OCFile.ROOT_PATH == syncFolderRemotePath - - if (currentDir == null && !isSyncFolderRemotePathRoot) { - handleRemovedFolder(syncFolderRemotePath) - } else if (currentDir != null) { - currentFile = handleRemovedFileFromServer(currentFile, currentDir) - updateFileList(fileListFragment, currentDir, syncFolderRemotePath) - file = currentFile - } - - if (isSyncFolderRemotePathRoot) { - startMetadataSyncForRoot() - } - - handleSyncResult(event, syncResult) - DataHolderUtil.getInstance().delete(id) - handleScrollBehaviour(fileListFragment) - } - - private fun handleRemovedFileFromServer(currentFile: OCFile?, currentDir: OCFile?): OCFile? { - if (currentFile == null && file?.isFolder == false) { - resetScrollingAndUpdateActionBar() - return currentDir - } - - return currentFile - } - - private fun handleRemovedFolder(syncFolderRemotePath: String?) { - SnackbarUtil.show(this, R.string.sync_current_folder_was_removed, syncFolderRemotePath) - fileListFragment?.let { - it.parentFolderFinder.getParentOnFirstParentRemoved(syncFolderRemotePath, storageManager)?.let { target -> - it.listDirectory(target, MainApp.isOnlyOnDevice()) - updateActionBarTitleAndHomeButton(target) - file = target - } - } - } - - private fun updateFileList( - ocFileListFragment: OCFileListFragment?, - currentDir: OCFile, - syncFolderRemotePath: String? - ) { - if (currentDir.remotePath != syncFolderRemotePath) return - - ocFileListFragment?.listDirectory(currentDir, MainApp.isOnlyOnDevice()) - } - - private fun handleScrollBehaviour(ocFileListFragment: OCFileListFragment?) { - ocFileListFragment ?: return - if (syncState == EmptyListState.LOADING || ocFileListFragment.isLoading) return - - if (ocFileListFragment.isEmpty) { - lockScrolling() - return - } - - resetScrolling(false) - } - - private fun handleSyncResult(event: String?, syncResult: Any?) { - if (RefreshFolderOperation.EVENT_SINGLE_FOLDER_CONTENTS_SYNCED != event || syncResult == null) { - return - } - - if (syncResult is RemoteOperationResult<*> && syncResult.isSuccess) { - hideInfoBox() - return - } - - handleFailedSyncResult(syncResult) - } - - private fun handleFailedSyncResult(syncResult: Any?) { - if (checkForRemoteOperationError(syncResult)) { - requestCredentialsUpdate() - } else { - handleNonCredentialSyncErrors(syncResult) - } - } - - private fun handleNonCredentialSyncErrors(syncResult: Any?) { - if (syncResult !is RemoteOperationResult<*>) return - - when (syncResult.code) { - RemoteOperationResult.ResultCode.SSL_RECOVERABLE_PEER_UNVERIFIED -> showUntrustedCertDialog(syncResult) - RemoteOperationResult.ResultCode.MAINTENANCE_MODE -> showInfoBox(R.string.maintenance_mode) - RemoteOperationResult.ResultCode.NO_NETWORK_CONNECTION -> showInfoBox(R.string.offline_mode) - RemoteOperationResult.ResultCode.HOST_NOT_AVAILABLE -> showInfoBox(R.string.host_not_available) - RemoteOperationResult.ResultCode.SIGNING_TOS_NEEDED -> showTermsOfServiceDialog() - RemoteOperationResult.ResultCode.OUT_OF_MEMORY -> syncState = EmptyListState.OUT_OF_MEMORY - else -> {} - } - } - - private fun showTermsOfServiceDialog() { - if (supportFragmentManager.findFragmentByTag(DIALOG_TAG_SHOW_TOS) == null) { - TermsOfServiceDialog().show(supportFragmentManager, DIALOG_TAG_SHOW_TOS) - } - } - - private fun checkForRemoteOperationError(syncResult: Any?): Boolean = syncResult is RemoteOperationResult<*> && - ( - RemoteOperationResult.ResultCode.UNAUTHORIZED == syncResult.code || - (syncResult.isException && syncResult.exception is AuthenticatorException) - ) - - private fun onSyncFinished() { - if (syncState != EmptyListState.LOADING) return - - syncState = when { - MainApp.isOnlyOnDevice() -> EmptyListState.ONLY_ON_DEVICE - - listOfFilesFragment?.searchEvent?.searchType == SearchRemoteOperation.SearchType.FAVORITE_SEARCH -> - SearchType.FAVORITE_SEARCH - - else -> SearchType.NO_SEARCH - } - } - - // endregion - - /** - * Once the file upload has finished -> update view - */ - private inner class FileUploadCompletedReceiver : BroadcastReceiver() { - private val tag = "UploadFinishReceiver" - - override fun onReceive(context: Context?, intent: Intent) { - Log_OC.d(tag, "upload finish received broadcast") - - val uploadedRemotePath = intent.getStringExtra(FileUploadEventBroadcaster.EXTRA_REMOTE_PATH) - val accountName = intent.getStringExtra(FileUploadEventBroadcaster.EXTRA_ACCOUNT_NAME) - val sameAccount = accountName != null && accountName == getAccount()?.name - val currentDirRemotePath = getCurrentDir()?.remotePath - val isDescendant = - currentDirRemotePath != null && uploadedRemotePath?.startsWith(currentDirRemotePath) == true - - if (sameAccount && isDescendant) { - updateListOfFilesFragment() - } - - val uploadWasFine = intent.getBooleanExtra(FileUploadEventBroadcaster.EXTRA_UPLOAD_RESULT, false) - val currentFile = file - val oldRemotePath = intent.getStringExtra(FileUploadEventBroadcaster.EXTRA_OLD_REMOTE_PATH) - val renamedInUpload = currentFile != null && currentFile.remotePath == oldRemotePath - val sameFile = currentFile != null && (currentFile.remotePath == uploadedRemotePath || renamedInUpload) - - val fileDetailFragment = leftFragment as? FileDetailFragment - if (sameAccount && sameFile && fileDetailFragment != null) { - updateFileDetailsAfterUpload(fileDetailFragment, uploadedRemotePath, uploadWasFine, renamedInUpload) - } + val fileFragment = OCFileListFragment() + replaceLeftFragment(fileFragment) - refreshAlbumScreensAfterUpload() - } + supportFragmentManager.executePendingTransactions() - private fun updateFileDetailsAfterUpload( - fileDetailFragment: FileDetailFragment, - uploadedRemotePath: String?, - uploadWasFine: Boolean, - renamedInUpload: Boolean - ) { - if (uploadWasFine) { - file = storageManager.getFileByPath(uploadedRemotePath) - } else { - // TODO remove upload progress bar after upload failed. - Log_OC.d(TAG, "Remove upload progress bar after upload failed") - } + fileFragment.onItemClicked(result.file) + } - if (renamedInUpload && !uploadedRemotePath.isNullOrBlank()) { - val newName = File(uploadedRemotePath).name - SnackbarUtil.show(this@FileDisplayActivity, R.string.filedetails_renamed_in_upload_msg, newName) - } + fun showFile(selectedFile: OCFile?, message: String?) { + withOCFileListFragment { fragment -> + dismissLoadingDialog() - if (uploadWasFine || file?.fileExists() == true) { - fileDetailFragment.updateFileDetails(false, true) + if (message.isNullOrEmpty()) { + val current = getCurrentDir() + fragment.listDirectory(current, file, MainApp.isOnlyOnDevice()) + file = current + updateActionBarTitleAndHomeButton(null) } else { - onBackPressedDispatcher.onBackPressed() + fragment.view?.let { SnackbarUtil.show(it, message) } } - if (uploadWasFine) { - file?.let { forcePreviewIfImageOrText(it) } - } + selectedFile?.let(fragment::onItemClicked) } + } + // endregion - private fun forcePreviewIfImageOrText(uploadedFile: OCFile) { - when { - PreviewMediaFragment.canBePreviewed(uploadedFile) -> startImagePreview(uploadedFile, true) - PreviewTextFileFragment.canBePreviewed(uploadedFile) -> startTextPreview(uploadedFile, true) - } + // region Fragment navigation + val leftFragment: Fragment? + get() = supportFragmentManager.findFragmentByTag(TAG_LIST_OF_FILES) + + val listOfFilesFragment: OCFileListFragment? + get() = supportFragmentManager.findFragmentByTag(TAG_LIST_OF_FILES) as? OCFileListFragment ?: run { + Log_OC.e(TAG, "Access to unexisting list of files fragment") + null } - private fun refreshAlbumScreensAfterUpload() { - when { - isAlbumsFragment -> - (supportFragmentManager.findFragmentByTag(AlbumsFragment.TAG) as? AlbumsFragment)?.refreshAlbums() + private val topFileListFragment: OCFileListFragment? + get() = lastFragment() as? OCFileListFragment ?: listOfFilesFragment - isAlbumItemsFragment -> - (supportFragmentManager.findFragmentByTag(AlbumItemsFragment.TAG) as? AlbumItemsFragment) - ?.refreshData() - } - } - } + /** + * Determines which navigation drawer item should be selected. + * + * Resolution order: + * 1) Global app state (static flags in MainApp nav_personal and nav_on_device) + * 2) Currently visible fragment (and its active child) + * 3) Fallback to All Files + */ + override fun getMenuItemId(): Int = MainApp.getMenuItemId() ?: listOfFilesFragment?.menuItemId ?: R.id.nav_all_files - private enum class FileDownloadIndicator(val iconId: Int) { - Downloading(R.drawable.ic_synchronizing), - Downloaded(R.drawable.ic_synced) + override fun setFile(file: OCFile?) { + super.setFile(file) + file?.takeIf { it.isFolder }?.let { viewModel.setCurrentFolder(it.fileId) } } - private fun updateFileDownloadIndicator(state: FileDownloadIndicator, file: OCFile) { - ocFileListFragment?.adapter?.updateFileIndicator(state.iconId, file) + private fun replaceLeftFragment(fragment: Fragment, showSortListGroup: Boolean = true) { + isShowingDetailsFromPreview = false + prepareFragmentBeforeCommit(showSortListGroup) + commitFragment(fragment) } - // region FolderDownloadWorker observers - private fun onFolderFileDownloadProgress(fileId: Long, previousFileId: Long?) { - val adapter = ocFileListFragment?.adapter ?: return - previousFileId?.let { storageManager.getFileById(it) }?.let(adapter::replaceFile) - adapter.updateFileIndicator(fileId, FileDownloadIndicator.Downloading.iconId) - } + private fun prepareFragmentBeforeCommit(showSortListGroup: Boolean) { + searchView?.post { searchView?.setQuery(searchQuery, true) } + setDrawerIndicatorEnabled(false) - // endregion + // the media screen sets a subtitle that must not stay on other screens + clearToolbarSubtitle() - // region FileDownloadWorker receivers - private fun getFileFromFileDownloadWorker(intent: Intent): OCFile? { - val remotePath = intent.getStringExtra(FileDownloadEventBroadcaster.EXTRA_REMOTE_PATH) - return fileDataStorageManager.getFileByDecryptedRemotePath(remotePath) + showSortListGroup(showSortListGroup) } - private inner class FileDownloadStartedReceiver : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - Log_OC.d(TAG, "download worker started") - getFileFromFileDownloadWorker(intent)?.let { - updateFileDownloadIndicator(FileDownloadIndicator.Downloading, it) - } - handleDownloadWorkerState() + private fun commitFragment(fragment: Fragment): Boolean { + val fragmentManager = supportFragmentManager + if (!isActive() || fragmentManager.isDestroyed || fragmentManager.isStateSaved) { + Log_OC.d(TAG, "${fragment.javaClass.simpleName} not committed, skipping") + return false } - } - - private inner class FileDownloadCompletedReceiver : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent) { - Log_OC.d(TAG, "file download completed received") - getFileFromFileDownloadWorker(intent)?.let { - updateFileDownloadIndicator(FileDownloadIndicator.Downloaded, it) - } - if (fileIDForImmediatePreview == NO_IMMEDIATE_PREVIEW_FILE_ID) { - Log_OC.d(TAG, "updating ui for file download") - updateUIForFileDownload(intent) - return - } + fragmentManager.beginTransaction() + .addToBackStack(null) + .replace(R.id.left_fragment_container, fragment, TAG_LIST_OF_FILES) + .commit() - Log_OC.d(TAG, "updating ui immediate file preview") + Log_OC.d(TAG, "${fragment.javaClass.simpleName} committed, pending transaction") - val downloadedRemotePath = intent.getStringExtra(FileDownloadEventBroadcaster.EXTRA_REMOTE_PATH) - val currentFile = storageManager.getFileByDecryptedRemotePath(downloadedRemotePath) ?: return - if (fileIDForImmediatePreview != currentFile.fileId || !currentFile.isDown) return + return true + } - fileIDForImmediatePreview = NO_IMMEDIATE_PREVIEW_FILE_ID - if (PreviewMediaFragment.canBePreviewed(currentFile)) { - startImagePreview(currentFile, currentFile.isDown) - } else { - previewFile(currentFile, null) - } + private fun withOCFileListFragment(onFragmentReady: (OCFileListFragment) -> Unit) { + val currentLeftFragment = leftFragment + if (currentLeftFragment is OCFileListFragment) { + onFragmentReady(currentLeftFragment) + return } - private fun updateUIForFileDownload(intent: Intent) { - val downloadedRemotePath = intent.getStringExtra(FileDownloadEventBroadcaster.EXTRA_REMOTE_PATH) - val downloadBehaviour = intent.getStringExtra(FileDownloadEventBroadcaster.EXTRA_DOWNLOAD_BEHAVIOUR) - - if (isSameAccount(intent) && isDescendant(downloadedRemotePath)) { - updateListOfFilesFragment() - val intentAction = intent.action - if (intentAction != null && downloadedRemotePath != null) { - refreshDetailsFragmentIfVisible( - intentAction, - downloadedRemotePath, - intent.getBooleanExtra(FileDownloadEventBroadcaster.EXTRA_DOWNLOAD_RESULT, false) - ) - } - } + val listOfFiles = OCFileListFragment().apply { + arguments = Bundle().apply { putBoolean(OCFileListFragment.ARG_ALLOW_CONTEXTUAL_ACTIONS, true) } + } - waitingToSend?.let { previousWaitingToSend -> - // update file after downloading - val updatedWaitingToSend = storageManager.getFileByRemoteId(previousWaitingToSend.remoteId) - waitingToSend = updatedWaitingToSend - if (updatedWaitingToSend?.isDown == true && OCFileListFragment.DOWNLOAD_SEND == downloadBehaviour) { - val packageName = intent.getStringExtra(FileDownloadEventBroadcaster.EXTRA_PACKAGE_NAME) ?: return - val activityName = intent.getStringExtra(FileDownloadEventBroadcaster.EXTRA_ACTIVITY_NAME) ?: return - sendDownloadedFile(packageName, activityName) - } - } + runOnUiThread { + val fragmentManager = supportFragmentManager + if (fragmentManager.isStateSaved || fragmentManager.isDestroyed) return@runOnUiThread - waitingToPreview?.let { previousWaitingToPreview -> - val updatedWaitingToPreview = storageManager.getFileByRemoteId(previousWaitingToPreview.remoteId) - waitingToPreview = updatedWaitingToPreview - if (updatedWaitingToPreview?.isDown == true && - EditImageActivity.OPEN_IMAGE_EDITOR == downloadBehaviour - ) { - startImageEditor(updatedWaitingToPreview) - } + prepareFragmentBeforeCommit(true) + if (commitFragment(listOfFiles)) { + fragmentManager.executePendingTransactions() + onFragmentReady(listOfFiles) } } + } - fun isDescendant(downloadedRemotePath: String?): Boolean { - val currentDirRemotePath = getCurrentDir()?.remotePath ?: return false - return downloadedRemotePath?.startsWith(currentDirRemotePath) == true + fun showFileActions(file: OCFile?) { + dismissLoadingDialog() + withOCFileListFragment { fragment -> + browseUp(fragment) + file?.let { fragment.onOverflowIconClicked(it, null) } } + } - fun isSameAccount(intent: Intent): Boolean { - val accountName = intent.getStringExtra(FileDownloadEventBroadcaster.EXTRA_ACCOUNT_NAME) - return accountName != null && accountName == account?.name + override fun showFiles(onDeviceOnly: Boolean, personalFiles: Boolean) { + super.showFiles(onDeviceOnly, personalFiles) + showOrRefreshFileList() + } + + private fun showOrRefreshFileList() { + val fileList = listOfFilesFragment + if (fileList != null && + fileList !is GalleryFragment && + fileList !is SharedListFragment && + // needed to show the offline files screen when navigating from Albums to Offline Files + !isAlbumsFragment && + !isAlbumItemsFragment + ) { + fileList.refreshDirectory() + } else { + replaceLeftFragment(OCFileListFragment()) } } - // endregion fun browseToRoot(performRefresh: Boolean = true) { listOfFilesFragment?.let { @@ -1830,33 +1090,70 @@ class FileDisplayActivity : startMetadataSyncForCurrentDir() } - /** - * Shows the information of the [OCFile] received as a parameter. - * - * @param file [OCFile] whose details will be shown - */ + private fun browseUp(listOfFiles: OCFileListFragment) { + listOfFiles.onBrowseUp() + val currentFile = listOfFiles.currentFile + + file = currentFile + + currentFile?.let { + listOfFiles.setFabVisible(it.canCreateFileAndFolder()) + listOfFiles.registerFabListener() + } + + resetScrollingAndUpdateActionBar() + startMetadataSyncForCurrentDir() + } + override fun showDetails(file: OCFile?) { showDetails(file, 0) } - /** - * Shows the information of the [OCFile] received as a parameter. - * - * @param file [OCFile] whose details will be shown - * @param activeTab the active tab in the details view - */ override fun showDetails(file: OCFile?, activeTab: Int) { val currentUser = user.orElse(null) ?: return resetScrolling(true) - val detailFragment = FileDetailFragment.newInstance(file, currentUser, activeTab) - setLeftFragment(detailFragment, false) + replaceLeftFragment(FileDetailFragment.newInstance(file, currentUser, activeTab), showSortListGroup = false) // swap now, so no frame shows the list without its sort row supportFragmentManager.executePendingTransactions() updateActionBarForFile(file) } + fun performUnifiedSearch(query: String, listOfHiddenFiles: ArrayList?) { + val unifiedSearchFragment = + UnifiedSearchFragment.newInstance(query, listOfHiddenFiles, currentDir?.decryptedRemotePath) + replaceLeftFragment(unifiedSearchFragment, showSortListGroup = false) + } + + fun refreshCurrentDirectory() { + val currentDir = getCurrentDir()?.let { storageManager.getFileByDecryptedRemotePath(it.remotePath) } + topFileListFragment?.listDirectory(currentDir, MainApp.isOnlyOnDevice()) + } + + fun updateListOfFilesFragment() { + listOfFilesFragment?.listDirectory(MainApp.isOnlyOnDevice()) + } + + fun resetSearchView() { + listOfFilesFragment?.isSearchFragment = false + } + + override fun onSortingOrderChosen(selection: FileSortOrder?) { + listOfFilesFragment?.fileListLayoutManager?.sortFiles(selection) + } + + override fun onFilesRemoved() { + refreshCurrentDirectory() + } + // endregion + + // region Toolbar and scrolling + protected fun resetScrollingAndUpdateActionBar() { + updateActionBarTitleAndHomeButton(file) + resetScrolling(true) + } + /** * Prevents content scrolling and toolbar collapse */ @@ -1868,587 +1165,819 @@ class FileDisplayActivity : } } - /** - * Resets content scrolling and toolbar collapse - */ - @VisibleForTesting - fun resetScrolling(expandAppBar: Boolean) { - binding.appbar.toolbarFrame.updateLayoutParams { - scrollFlags = - AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL or AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS - } - if (expandAppBar) { - binding.appbar.appbar.setExpanded(true, false) - } - } + /** + * Resets content scrolling and toolbar collapse + */ + @VisibleForTesting + fun resetScrolling(expandAppBar: Boolean) { + binding.appbar.toolbarFrame.updateLayoutParams { + scrollFlags = + AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL or AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS + } + if (expandAppBar) { + binding.appbar.appbar.setExpanded(true, false) + } + } + + public override fun updateActionBarTitleAndHomeButton(chosenFile: OCFile?) { + super.updateActionBarTitleAndHomeButton(chosenFile ?: file) + supportInvalidateOptionsMenu() + } + + override fun isDrawerIndicatorAvailable(): Boolean = isRoot(getCurrentDir()) + + fun configureToolbarForPreview(file: OCFile) { + lockScrolling() + updateActionBarForFile(file) + } + + fun setMainFabVisible(visible: Boolean) { + binding.fabMain.isVisible = visible + } + // endregion + + // region Options menu and search + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.activity_file_display, menu) + + menu.findItem(R.id.action_select_all).isVisible = false + val searchMenuItem = menu.findItem(R.id.action_search) + searchView = searchMenuItem.actionView as? SearchView + searchMenuItem.isVisible = false + mSearchText.setOnClickListener { + showSearchView() + searchView?.postDelayed({ + searchView?.isIconified = false + searchView?.requestFocusFromTouch() + }, SEARCH_VIEW_FOCUS_DELAY) + } + + drawerMenuItemsToShowHide = listOf(searchMenuItem) + + searchView?.let { setupSearchView(it) } + + return super.onCreateOptionsMenu(menu) + } + + override fun onPrepareOptionsMenu(menu: Menu?): Boolean { + val drawerOpen = isDrawerOpen + drawerMenuItemsToShowHide.forEach { it.isVisible = !drawerOpen } + + menu?.findItem(R.id.action_current_folder_actions)?.isVisible = canShowCurrentFolderActions() + + return super.onPrepareOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { + android.R.id.home -> { + if (shouldOpenDrawer()) openDrawer() else navigateBack() + true + } + + R.id.action_select_all -> { + listOfFilesFragment?.selectAllFiles(true) + true + } + + R.id.action_current_folder_actions -> { + openCurrentFolderActions() + true + } + + else -> super.onOptionsItemSelected(item) + } + + private fun canShowCurrentFolderActions(): Boolean = leftFragment is OCFileListFragment && + !isDrawerOpen && + !isSearchOpen() && + !isRoot(getCurrentDir()) && + listOfFilesFragment?.isSearchFragment == false && + !isAlbumItemsFragment + + private fun openCurrentFolderActions() { + if (canShowCurrentFolderActions()) { + listOfFilesFragment?.openCurrentFolderActionsMenu() + } + } + + private fun shouldOpenDrawer(): Boolean = !isDrawerOpen && + !isSearchOpen() && + isRoot(getCurrentDir()) && + leftFragment is OCFileListFragment && + !isAlbumItemsFragment - public override fun updateActionBarTitleAndHomeButton(chosenFile: OCFile?) { - // if no file is passed, current file decides - super.updateActionBarTitleAndHomeButton(chosenFile ?: file) - supportInvalidateOptionsMenu() - } + private fun setupSearchView(view: SearchView) { + viewThemeUtils.androidx.themeToolbarSearchView(view) - override fun isDrawerIndicatorAvailable(): Boolean = isRoot(getCurrentDir()) + if (searchQuery.isNotEmpty()) { + view.post { restoreSearchQuery() } + } - private fun observeWorkerState() { - lifecycleScope.launch { - viewModel.observeOfflineWorker(onComplete = { - refreshCurrentDirectory() - }) + view.setOnCloseListener { + onSearchViewClosed(view) + true } - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.observeFolderDownloadWorker( - onFileProgress = ::onFolderFileDownloadProgress, - onCompleted = ::updateListOfFilesFragment - ) - } + view.findViewById(androidx.appcompat.R.id.search_edit_frame)?.let { + disableDrawerIndicatorWhenVisible(it) } } - fun canPreviewInMediaPager(file: OCFile?): Boolean = - PreviewMediaFragment.canBePreviewed(file) || (file != null && MimeTypeUtil.isVideo(file)) - - fun previewImageWithSearchContext( - file: OCFile, - searchFragment: Boolean, - currentSearchType: SearchType?, - sourceView: View? - ) { - val type = if (searchFragment) { - when (currentSearchType) { - SearchType.FAVORITE_SEARCH -> VirtualFolderType.FAVORITE - SearchType.GALLERY_SEARCH -> VirtualFolderType.GALLERY - else -> VirtualFolderType.NONE - } - } else { - null + private fun restoreSearchQuery() { + searchView?.run { + isIconified = false + setQuery(searchQuery, true) + clearFocus() } + } - val mediaState = if (type == VirtualFolderType.GALLERY) { - (listOfFilesFragment as? GalleryFragment)?.currentMediaState - } else { - null + private fun onSearchViewClosed(view: SearchView) { + if (!view.query.isNullOrEmpty()) { + view.post { searchView?.setQuery("", true) } + return } - val showPreview = file.isDown || MimeTypeUtil.isVideo(file) - startImagePreview(file, showPreview, type, mediaState, sourceView) - } + view.onActionViewCollapsed() + setDrawerIndicatorEnabled(isDrawerIndicatorAvailable) // order matters + supportActionBar?.setDisplayHomeAsUpEnabled(true) + mDrawerToggle.syncState() - fun previewFile(file: OCFile, setFabVisible: CompletionCallback?) { - if (!file.isDown) { - Log_OC.d(TAG, "File is not downloaded, cannot be previewed") - return + listOfFilesFragment?.run { + isSearchFragment = false + refreshDirectory() } + } - when { - MimeTypeUtil.isVCard(file) -> startContactListFragment(file) + private fun disableDrawerIndicatorWhenVisible(searchEditFrame: View) { + searchEditFrame.viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener { + var oldVisibility: Int = UNKNOWN_VISIBILITY - MimeTypeUtil.isPDF(file) -> startPdfPreview(file) + override fun onGlobalLayout() { + val currentVisibility = searchEditFrame.visibility + if (currentVisibility == oldVisibility) return - PreviewTextFileFragment.canBePreviewed(file) -> { - setFabVisible?.onComplete(false) - startTextPreview(file, false) + if (currentVisibility == View.VISIBLE) { + setDrawerIndicatorEnabled(false) + } + oldVisibility = currentVisibility } + }) + } - canPreviewInAudioPlayer(file) -> { - setFabVisible?.onComplete(false) - startAudioPreview(file, true, false) - } + fun isSearchOpen(): Boolean = + searchView?.findViewById(androidx.appcompat.R.id.search_edit_frame)?.isVisible == true - else -> fileOperationsHelper.openFile(file) - } - } + private fun closeSearch() { + val currentLeftFragment = leftFragment + val currentSearchView = searchView ?: return + if (!isSearchOpen()) return - fun refreshCurrentDirectory() { - val currentDir = getCurrentDir()?.let { storageManager.getFileByDecryptedRemotePath(it.remotePath) } - fileListFragment?.listDirectory(currentDir, MainApp.isOnlyOnDevice()) - } + currentSearchView.run { + clearFocus() + val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager + inputMethodManager?.hideSoftInputFromWindow(windowToken, 0) + setQuery("", false) + onActionViewCollapsed() + } - private fun handleDownloadWorkerState() { - val previousWaitingToPreview = waitingToPreview ?: return - if (storageManager == null) return + if (isRoot(getCurrentDir()) && currentLeftFragment is OCFileListFragment) { + currentLeftFragment.adapter?.let { adapter -> + currentLeftFragment.performSearch("", adapter.listOfHiddenFiles, true) + } + hideSearchView(getCurrentDir()) + setDrawerIndicatorEnabled(isDrawerIndicatorAvailable) + } - val updatedWaitingToPreview = storageManager.getFileById(previousWaitingToPreview.fileId) - waitingToPreview = updatedWaitingToPreview - if (updatedWaitingToPreview?.isDown == false) { - requestForDownload() + if (currentLeftFragment is UnifiedSearchFragment) { + showSortListGroup(false) + supportFragmentManager.popBackStack() } } - override fun newTransferenceServiceConnection(): ServiceConnection = ListServiceConnection() - - /** - * Defines callbacks for service binding, passed to bindService() - * TODO: Check if this can be removed since download and uploads uses work manager now. - */ - private inner class ListServiceConnection : ServiceConnection { - override fun onServiceConnected(name: ComponentName?, service: IBinder?) = Unit + fun setSearchQuery(query: String) { + searchQuery = query + } + // endregion - override fun onServiceDisconnected(component: ComponentName) { - if (component == ComponentName(this@FileDisplayActivity, FileDownloadWorker::class.java)) { - Log_OC.d(TAG, "Download service disconnected") + // region Back navigation + private fun registerBackPressCallback() { + onBackPressedDispatcher.addCallback( + this, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + // disabled while navigating, so a nested back press is handled by the system + navigateBack(before = { isEnabled = false }, after = { isEnabled = true }) + } } - } + ) } - /** - * Updates the view associated to the activity after the finish of some operation over files in the current - * account. - * - * @param operation Removal operation performed. - * @param result Result of the removal. - */ - override fun onRemoteOperationFinish(operation: RemoteOperation<*>?, result: RemoteOperationResult<*>) { - super.onRemoteOperationFinish(operation, result) + private fun navigateBack(before: () -> Unit = {}, after: () -> Unit = {}) { + val currentLeftFragment = leftFragment - when (operation) { - is RemoveFileOperation -> onRemoveFileOperationFinish(operation, result) + before() + when { + isSearchOpen() -> closeSearch() - is RenameFileOperation -> onRenameFileOperationFinish(operation, result) + isDrawerOpen -> closeDrawer() - is SynchronizeFileOperation -> onSynchronizeFileOperationFinish(operation, result) + isShowingDetailsFromPreview && currentLeftFragment is FileDetailFragment -> + NavigationAnimator(this).finishWithSlideDown() - is CreateFolderOperation -> onCreateFolderOperationFinish(operation, result) + isAlbumItemsFragment -> popBackStackAndResetUi() - is MoveFileOperation -> onMoveFileOperationFinish(operation, result) + currentLeftFragment is OCFileListFragment -> navigateBackInFileList(currentLeftFragment) - is CopyFileOperation -> onCopyFileOperationFinish(operation, result) + else -> popBackStackAndResetUi() + } + after() + } - is RestoreFileVersionRemoteOperation -> onRestoreFileVersionOperationFinish(result) + private fun navigateBackInFileList(fragment: OCFileListFragment) { + if (!isRoot(getCurrentDir())) { + browseUp(fragment) + return + } - is CreateNewAlbumRemoteOperation -> albumOperationListener.onCreateAlbumOperationFinish(operation, result) + if (fragment.shouldNavigateBackToAllFiles()) navigateToAllFiles() else finish() + } - is CopyFileToAlbumOperation -> albumOperationListener.onCopyAlbumFileOperationFinish(operation, result) + private fun popBackStackAndResetUi() { + binding.fabMain.setImageResource(R.drawable.ic_plus) + resetScrolling(true) + showSortListGroup(false) + showBottomNavigationBar(true) + supportFragmentManager.popBackStack() + } + // endregion - is RenameAlbumRemoteOperation -> albumOperationListener.onRenameAlbumOperationFinish(operation, result) + // region Activity results + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + when { + data != null && requestCode in selectContentFromAppsRequestCodes && + resultCode in selectContentFromAppsResultCodes -> + uploadHandler.uploadContentFromApps( + data, + resultCode, + autoRename = requestCode == REQUEST_CODE__SELECT_CONTENT_FROM_APPS_AUTO_RENAME + ) - is RemoveAlbumRemoteOperation -> albumOperationListener.onRemoveAlbumOperationFinish(operation, result) + data != null && requestCode == REQUEST_CODE__SELECT_FILES_FROM_FILE_SYSTEM && + resultCode in selectFilesFromFileSystemResultCodes -> + uploadHandler.uploadFilesFromFileSystem(data, resultCode) - is PublicShareLinkAlbumRemoteOperation -> - albumOperationListener.onAlbumPublicLinkOperationFinish(operation, result) + requestCode in uploadFromCameraRequestCodes && resultCode in uploadFromCameraResultCodes -> + uploadHandler.uploadFromCamera(isVideo = requestCode == REQUEST_CODE__UPLOAD_FROM_VIDEO_CAMERA) + + requestCode == REQUEST_CODE__MOVE_OR_COPY_FILES && resultCode == RESULT_OK -> + listOfFilesFragment?.exitSelectionMode() + + else -> super.onActivityResult(requestCode, resultCode, data) } } + // endregion - private val fileListFragment: OCFileListFragment? - get() = lastFragment() as? OCFileListFragment ?: listOfFilesFragment + // region Broadcast receivers + private fun registerReceivers() { + Log_OC.d(TAG, "registering receivers") - private fun refreshGalleryFragmentIfNeeded() { - if (fileListFragment is GalleryFragment) { - startPhotoSearch(R.id.nav_gallery) + localBroadcastManager.run { + registerReceiver( + uploadCompletedReceiver, + IntentFilter(FileUploadEventBroadcaster.ACTION_UPLOAD_COMPLETED) + ) + registerReceiver( + downloadStartedReceiver, + IntentFilter(FileDownloadEventBroadcaster.ACTION_DOWNLOAD_ENQUEUED) + ) + registerReceiver( + downloadCompletedReceiver, + IntentFilter(FileDownloadEventBroadcaster.ACTION_DOWNLOAD_COMPLETED) + ) + + val syncIntentFilter = IntentFilter(FileSyncAdapter.EVENT_FULL_SYNC_START).apply { + addAction(FileSyncAdapter.EVENT_FULL_SYNC_END) + addAction(FileSyncAdapter.EVENT_FULL_SYNC_FOLDER_CONTENTS_SYNCED) + addAction(RefreshFolderOperation.EVENT_SINGLE_FOLDER_CONTENTS_SYNCED) + addAction(RefreshFolderOperation.EVENT_SINGLE_FOLDER_SHARES_SYNCED) + } + registerReceiver(syncReceiver, syncIntentFilter) } } - private fun refreshShowDetails() { - val details = leftFragment as? FileFragment ?: return + private fun unregisterReceivers() { + Log_OC.d(TAG, "unregistering receivers") - details.file?.let { - val refreshedFile = storageManager.getFileByPath(it.remotePath) - if (details is PreviewTextFragment) { - // Refresh OCFile of the fragment - (details as? PreviewTextFileFragment)?.updateFile(refreshedFile) - } else { - showDetails(refreshedFile) - } + localBroadcastManager.run { + unregisterReceiver(syncReceiver) + unregisterReceiver(uploadCompletedReceiver) + unregisterReceiver(downloadStartedReceiver) + unregisterReceiver(downloadCompletedReceiver) } - supportInvalidateOptionsMenu() } - val deleteBatchTracker = DeleteBatchTracker(onAllDeletesFinished = { - (leftFragment as? GalleryFragment)?.onRefresh() - }) + private fun isCurrentAccount(accountName: String?): Boolean = accountName != null && accountName == account?.name + + private fun isInCurrentDir(remotePath: String?): Boolean { + val currentDirRemotePath = getCurrentDir()?.remotePath ?: return false + return remotePath?.startsWith(currentDirRemotePath) == true + } + // endregion + + // region Folder sync /** - * Updates the view associated to the activity after the finish of an operation trying to remove a file. + * Starts a "light sync" of the given folder: properties of its files (including shares) are updated, but only + * the contents of files marked to be kept in sync are downloaded. * - * @param operation Removal operation performed. - * @param result Result of the removal. + * @param ignoreETag fetches the folder even if its eTag didn't change + * @param ignoreFocus reloads the file list even without window focus, e.g. while a detail view has the focus */ - private fun onRemoveFileOperationFinish(operation: RemoveFileOperation, result: RemoteOperationResult<*>) { - deleteBatchTracker.onSingleDeleteFinished() - if (!result.isSuccess && result.isSslRecoverableException) { - showUntrustedCertDialogFor(result) - return - } + @JvmOverloads + fun startSyncFolderOperation(folder: OCFile?, ignoreETag: Boolean, ignoreFocus: Boolean = false) { + Log_OC.d(TAG, "startSyncFolderOperation called, ignoreEtag: $ignoreETag, ignoreFocus: $ignoreFocus") - if (!result.isSuccess) { - Log_OC.e(TAG, "deletion failed") - return + if (searchQuery.isNotEmpty() || user.isEmpty) return + + val syncFolder = Runnable { folder?.let { refreshFolder(it, ignoreETag) } } + + // The refresh must not run while another window floats over the activity, e.g. a dialog that is being + // dismissed or a rotation. Rather than waiting a fixed delay run right away when it already has focus + // and replay the request on the next focus gain. + if (ignoreFocus || hasWindowFocus()) { + pendingSyncFolderOperation = null + syncFolder.run() + } else { + pendingSyncFolderOperation = syncFolder } + } - val removedFile = operation.file - file?.let { playbackModel.stopPlaying(it) } - val currentLeftFragment = leftFragment + private fun refreshFolder(folder: OCFile, ignoreETag: Boolean) { + val currentUser = user.orElse(null) ?: return - // check if file is still available, if so do nothing - val fileAvailable = storageManager.fileExists(removedFile.fileId) - if (currentLeftFragment is FileFragment && !fileAvailable && removedFile == currentLeftFragment.file) { - file = storageManager.getFileById(removedFile.parentId) - resetScrollingAndUpdateActionBar() - } + syncState = EmptyListState.LOADING - if (currentLeftFragment is OCFileListFragment) { - updateFileListAfterRemoval(currentLeftFragment, removedFile, operation.onlyLocalCopy) - } + RefreshFolderOperation( + folder, + System.currentTimeMillis(), + false, + ignoreETag, + storageManager, + currentUser, + applicationContext + ).execute( + account, + this, + { _, _ -> onSyncFinished() }, + handler, + null + ) - supportInvalidateOptionsMenu() - fetchRecommendedFilesIfNeeded(ignoreETag = true, currentDir) + fetchRecommendedFilesIfNeeded(ignoreETag, folder) } - private fun updateFileListAfterRemoval(fragment: OCFileListFragment, removedFile: OCFile, onlyLocalCopy: Boolean) { - val adapter = fragment.adapter ?: return - if (onlyLocalCopy) { - adapter.notifyItemChanged(removedFile) - return - } + private fun syncCurrentFolder(ignoreETag: Boolean, ignoreFocus: Boolean) { + val listOfFiles = listOfFilesFragment ?: return + if (listOfFiles.isSearchFragment) return - adapter.removeFile(removedFile) - if (adapter.isEmpty) { - val emptyState = fragment.searchEvent?.toSearchType() ?: SearchType.NO_SEARCH - fragment.setEmptyListMessage(emptyState) - } + val folder = listOfFiles.currentFile ?: return + startSyncFolderOperation(folder, ignoreETag, ignoreFocus) } - private fun showUntrustedCertDialogFor(result: RemoteOperationResult<*>) { - mLastSslUntrustedServerResult = result - showUntrustedCertDialog(result) + override fun onRefresh(ignoreETag: Boolean) { + syncCurrentFolder(ignoreETag, ignoreFocus = false) } - override fun onAutoUploadFolderRemoved( - entities: List, - filesToRemove: List, - onlyLocalCopy: Boolean - ) { - val dialog = ConfirmationDialogFragment.newInstance( - messageResId = R.string.auto_upload_delete_dialog_description, - messageArguments = null, - titleResId = R.string.auto_upload_delete_dialog_title, - titleIconId = R.drawable.ic_info, - positiveButtonTextId = R.string.common_delete, - negativeButtonTextId = R.string.common_cancel, - neutralButtonTextId = NO_NEUTRAL_BUTTON - ) + override fun onRefresh() { + syncCurrentFolder(ignoreETag = true, ignoreFocus = false) + } - dialog.setOnConfirmationListener(object : ConfirmationDialogFragment.ConfirmationDialogFragmentListener { - override fun onConfirmation(callerTag: String?) { - removeAutoUploadFolders(entities, filesToRemove, onlyLocalCopy) - } + override fun onSavedCertificate() { + startSyncFolderOperation(getCurrentDir(), false) + } - override fun onNeutral(callerTag: String?) = Unit - override fun onCancel(callerTag: String?) = Unit - }) + @SuppressLint("VisibleForTests") + private fun onSyncBroadcastReceived(intent: Intent) { + try { + val event = intent.action + Log_OC.d(TAG, "Received broadcast $event") - if (isDialogFragmentReady(dialog)) { - dialog.show(supportFragmentManager, null) - } - } + val accountName = intent.getStringExtra(FileSyncAdapter.EXTRA_ACCOUNT_NAME) + val syncFolderRemotePath = intent.getStringExtra(FileSyncAdapter.EXTRA_FOLDER_PATH) + val syncResultId = intent.getStringExtra(FileSyncAdapter.EXTRA_RESULT) + val syncResult = DataHolderUtil.getInstance().retrieve(syncResultId) + val isSameAccount = isCurrentAccount(accountName) && storageManager != null + val fileList = listOfFilesFragment - private fun removeAutoUploadFolders( - entities: List, - filesToRemove: List, - onlyLocalCopy: Boolean - ) { - lifecycleScope.launch(Dispatchers.IO) { - entities.mapNotNull { it.id?.toLong() }.forEach { - fileUploadHelper.removeEntityFromUploadEntities(it) - syncedFolderProvider.deleteSyncedFolder(it) + if (isSameAccount) { + handleSyncEvent(event, syncFolderRemotePath, syncResultId, fileList, syncResult) } - connectivityService.isNetworkAndServerAvailable { isAvailable -> - when { - isAvailable -> fileOperationsHelper?.removeFiles(filesToRemove, onlyLocalCopy, true) - onlyLocalCopy -> fileOperationsHelper?.removeFiles(filesToRemove, true, true) - else -> filesToRemove.forEach { fileDataStorageManager.addRemoveFileOfflineOperation(it) } - } - onFilesRemoved() + if (syncResult is RemoteOperationResult<*> && + syncResult.code == RemoteOperationResult.ResultCode.SSL_RECOVERABLE_PEER_UNVERIFIED + ) { + lastSslUntrustedServerResult = syncResult } + } catch (_: RuntimeException) { + deleteSyncResultSafely(intent) + } finally { + onSyncFinished() } } - private fun onRestoreFileVersionOperationFinish(result: RemoteOperationResult<*>) { - if (!result.isSuccess) { - SnackbarUtil.show(this, R.string.file_version_restored_error) - return + // avoid app crashes after changing the serial id of RemoteOperationResult in owncloud library + // with broadcast notifications pending to process + private fun deleteSyncResultSafely(intent: Intent) { + try { + DataHolderUtil.getInstance().delete(intent.getStringExtra(FileSyncAdapter.EXTRA_RESULT)) + } catch (_: RuntimeException) { + Log_OC.i(TAG, "Ignoring error deleting data") } + } - val restoredFile = getFile() + private fun handleSyncEvent( + event: String?, + syncFolderRemotePath: String?, + syncResultId: String?, + fileList: OCFileListFragment?, + syncResult: Any? + ) { + if (FileSyncAdapter.EVENT_FULL_SYNC_START == event) return - // delete old local copy - if (restoredFile?.isDown == true) { - fileOperationsHelper.removeFiles(listOf(restoredFile), true, true) + // Single folder events are only sent when the folder's content or sharees actually changed + val storedFile = file?.remotePath?.let { storageManager.getFileByPath(it) } + val currentDir = getCurrentDir()?.remotePath?.let { storageManager.getFileByPath(it) } + val isRootSynced = OCFile.ROOT_PATH == syncFolderRemotePath - // download new version, only if file was previously download - showSyncLoadingDialog(restoredFile.isFolder) - fileOperationsHelper.syncFileOrFolder(restoredFile) + if (currentDir == null && !isRootSynced) { + browseToParentOfRemovedFolder(syncFolderRemotePath) + } else if (currentDir != null) { + val fileAfterSync = resolveFileAfterSync(storedFile, currentDir) + fileList?.let { listDirectoryIfSynced(it, currentDir, syncFolderRemotePath) } + file = fileAfterSync } - val parent = restoredFile?.let { storageManager.getFileById(it.parentId) } - startSyncFolderOperation(parent, ignoreETag = true, ignoreFocus = true) + if (isRootSynced) { + startMetadataSyncForRoot() + } - (leftFragment as? FileDetailFragment)?.fileDetailActivitiesFragment?.reload() + handleSyncResult(event, syncResult) + DataHolderUtil.getInstance().delete(syncResultId) + fileList?.let { updateScrollingAfterSync(it) } } - private fun showErrorCauseMessage(result: RemoteOperationResult<*>, operation: RemoteOperation<*>?) { - try { - SnackbarUtil.show(this, ErrorMessageAdapter.getErrorCauseMessage(result, operation, resources)) - } catch (e: Resources.NotFoundException) { - Log_OC.e(TAG, "Error while trying to show fail message ", e) + private fun resolveFileAfterSync(storedFile: OCFile?, currentDir: OCFile): OCFile? { + if (storedFile == null && file?.isFolder == false) { + resetScrollingAndUpdateActionBar() + return currentDir } + + return storedFile } - /** - * Updates the view associated to the activity after the finish of an operation trying to move a file. - * - * @param operation Move operation performed. - * @param result Result of the move operation. - */ - private fun onMoveFileOperationFinish(operation: MoveFileOperation?, result: RemoteOperationResult<*>) { - if (!result.isSuccess) { - showErrorCauseMessage(result, operation) - } + private fun browseToParentOfRemovedFolder(removedFolderRemotePath: String?) { + SnackbarUtil.show(this, R.string.sync_current_folder_was_removed, removedFolderRemotePath) + val fileList = topFileListFragment ?: return + val parent = fileList.parentFolderFinder + .getParentOnFirstParentRemoved(removedFolderRemotePath, storageManager) ?: return + + fileList.listDirectory(parent, MainApp.isOnlyOnDevice()) + updateActionBarTitleAndHomeButton(parent) + file = parent } - /** - * Updates the view associated to the activity after the finish of an operation trying to copy a file. - * - * @param operation Copy operation performed. - * @param result Result of the copy operation. - */ - private fun onCopyFileOperationFinish(operation: CopyFileOperation?, result: RemoteOperationResult<*>) { - if (!result.isSuccess) { - showErrorCauseMessage(result, operation) + private fun listDirectoryIfSynced(fileList: OCFileListFragment, currentDir: OCFile, syncedRemotePath: String?) { + if (currentDir.remotePath != syncedRemotePath) return + + fileList.listDirectory(currentDir, MainApp.isOnlyOnDevice()) + } + + private fun updateScrollingAfterSync(fileList: OCFileListFragment) { + if (syncState == EmptyListState.LOADING || fileList.isLoading) return + + if (fileList.isEmpty) { + lockScrolling() return } - updateListOfFilesFragment() - refreshGalleryFragmentIfNeeded() + resetScrolling(false) } - /** - * Updates the view associated to the activity after the finish of an operation trying to rename a file. - * - * @param operation Renaming operation performed. - * @param result Result of the renaming. - */ - private fun onRenameFileOperationFinish(operation: RenameFileOperation, result: RemoteOperationResult<*>) { - val currentUser = user.orElse(null) - val renamedFile = operation.file - if (!result.isSuccess || currentUser == null) { - SnackbarUtil.show(this, ErrorMessageAdapter.getErrorCauseMessage(result, operation, resources)) + private fun handleSyncResult(event: String?, syncResult: Any?) { + if (RefreshFolderOperation.EVENT_SINGLE_FOLDER_CONTENTS_SYNCED != event) return + if (syncResult !is RemoteOperationResult<*>) return - if (result.isSslRecoverableException) { - showUntrustedCertDialogFor(result) - } - return + when { + syncResult.isSuccess -> hideInfoBox() + syncResult.isAuthenticationError() -> requestCredentialsUpdate() + else -> showSyncError(syncResult) } + } + + private fun RemoteOperationResult<*>.isAuthenticationError(): Boolean = + code == RemoteOperationResult.ResultCode.UNAUTHORIZED || (isException && exception is AuthenticatorException) - (leftFragment as? FileFragment)?.let { - onRenameFileOperationFinishForFileFragment(it, renamedFile, currentUser) + private fun showSyncError(syncResult: RemoteOperationResult<*>) { + when (syncResult.code) { + RemoteOperationResult.ResultCode.SSL_RECOVERABLE_PEER_UNVERIFIED -> showUntrustedCertDialog(syncResult) + RemoteOperationResult.ResultCode.MAINTENANCE_MODE -> showInfoBox(R.string.maintenance_mode) + RemoteOperationResult.ResultCode.NO_NETWORK_CONNECTION -> showInfoBox(R.string.offline_mode) + RemoteOperationResult.ResultCode.HOST_NOT_AVAILABLE -> showInfoBox(R.string.host_not_available) + RemoteOperationResult.ResultCode.SIGNING_TOS_NEEDED -> dialogs.showTermsOfServiceIfNotShown() + RemoteOperationResult.ResultCode.OUT_OF_MEMORY -> syncState = EmptyListState.OUT_OF_MEMORY + else -> Unit } + } - val parentDir = storageManager.getFileById(renamedFile.parentId) - val isCurrentDirParentDirOfGivenFile = parentDir != null && parentDir == currentDir + private fun onSyncFinished() { + if (syncState != EmptyListState.LOADING) return - // checking current dir against renamed file's parent will always fail when user is in Shared/Favorites root. - // thus check is current dir is root or not as well - if (currentDir?.isRootDirectory == true || isCurrentDirParentDirOfGivenFile) { - val fragment = fileListFragment + syncState = when { + MainApp.isOnlyOnDevice() -> EmptyListState.ONLY_ON_DEVICE - // OCFileSearchTask may still run during rename, so use a single refresh path to avoid - // flicker/inconsistent filenames. - if (fragment?.isSearchFragment == true) { - fragment.restartSearch() - } else { - fragment?.adapter?.updateFile(renamedFile) + listOfFilesFragment?.searchEvent?.searchType == SearchRemoteOperation.SearchType.FAVORITE_SEARCH -> + SearchType.FAVORITE_SEARCH + + else -> SearchType.NO_SEARCH + } + } + + private fun fetchRecommendedFilesIfNeeded(ignoreETag: Boolean, folder: OCFile?) { + val capability = capabilities.orElse(null) ?: return + if (folder?.isRootDirectory == false || capability.recommendations.isFalse) return + + val accountName = user.orElse(null)?.accountName ?: return + val fileList = listOfFilesFragment + lifecycleScope.launch { + val recommendedFiles = withContext(Dispatchers.IO) { + filesRepository.fetchRecommendedFiles(accountName, ignoreETag, storageManager) } + fileList?.adapter?.updateRecommendedFiles(recommendedFiles) } + } - refreshGalleryFragmentIfNeeded() - fetchRecommendedFilesIfNeeded(ignoreETag = true, currentDir) + private fun startMetadataSyncForRoot() { + backgroundJobManager.startMetadataSyncJob(OCFile.ROOT_PATH, folderAlreadySynced = true) + } + + private fun startMetadataSyncForCurrentDir() { + val currentDirPath = file?.decryptedRemotePath ?: return + backgroundJobManager.startMetadataSyncJob(currentDirPath) } + // endregion - private fun onRenameFileOperationFinishForFileFragment(fragment: FileFragment, ocFile: OCFile, user: User) { - if (fragment.file != ocFile) return + // region Uploads + private fun onUploadCompleted(intent: Intent) { + Log_OC.d(TAG, "upload finish received broadcast") - when (fragment) { - is FileDetailFragment -> { - fragment.updateFileDetails(ocFile, user) - showDetails(ocFile) - } + val uploadedRemotePath = intent.getStringExtra(FileUploadEventBroadcaster.EXTRA_REMOTE_PATH) + val isSameAccount = isCurrentAccount(intent.getStringExtra(FileUploadEventBroadcaster.EXTRA_ACCOUNT_NAME)) - is PreviewTextFileFragment -> { - fragment.updateFile(ocFile) - if (PreviewTextFileFragment.canBePreviewed(ocFile)) { - startTextPreview(ocFile, true) - } else { - fileOperationsHelper.openFile(ocFile) - } - } + if (isSameAccount && isInCurrentDir(uploadedRemotePath)) { + updateListOfFilesFragment() } - } - private fun onSynchronizeFileOperationFinish( - operation: SynchronizeFileOperation, - result: RemoteOperationResult<*> - ) { - if (!result.isSuccess || !operation.transferWasRequested) return + val uploadSucceeded = intent.getBooleanExtra(FileUploadEventBroadcaster.EXTRA_UPLOAD_RESULT, false) + val currentFile = file + val oldRemotePath = intent.getStringExtra(FileUploadEventBroadcaster.EXTRA_OLD_REMOTE_PATH) + val renamedInUpload = currentFile != null && currentFile.remotePath == oldRemotePath + val isSameFile = currentFile != null && (currentFile.remotePath == uploadedRemotePath || renamedInUpload) - val syncedFile = operation.localFile ?: return - onTransferStateChanged(syncedFile, true, true) - supportInvalidateOptionsMenu() - refreshShowDetails() + val fileDetailFragment = leftFragment as? FileDetailFragment + if (isSameAccount && isSameFile && fileDetailFragment != null) { + updateFileDetailsAfterUpload(fileDetailFragment, uploadedRemotePath, uploadSucceeded, renamedInUpload) + } + + refreshVisibleAlbumScreen() } - /** - * Updates the view associated to the activity after the finish of an operation trying create a new folder - * - * @param operation Creation operation performed. - * @param result Result of the creation. - */ - private fun onCreateFolderOperationFinish(operation: CreateFolderOperation, result: RemoteOperationResult<*>) { - if (!result.isSuccess) { - if (RemoteOperationResult.ResultCode.FOLDER_ALREADY_EXISTS == result.code) { - SnackbarUtil.show(this, R.string.folder_already_exists) - } else { - showErrorCauseMessage(result, operation) - } - return + private fun updateFileDetailsAfterUpload( + fileDetailFragment: FileDetailFragment, + uploadedRemotePath: String?, + uploadSucceeded: Boolean, + renamedInUpload: Boolean + ) { + if (uploadSucceeded) { + file = storageManager.getFileByPath(uploadedRemotePath) + } else { + // TODO remove upload progress bar after upload failed. + Log_OC.d(TAG, "Remove upload progress bar after upload failed") } - val fileListFragment = listOfFilesFragment - val createdFolder = storageManager.getFileByDecryptedRemotePath(operation.remotePath) - if (!operation.shouldEncrypt()) { - createdFolder?.let { fileListFragment?.onItemClicked(it) } - return + if (renamedInUpload && !uploadedRemotePath.isNullOrBlank()) { + val newName = File(uploadedRemotePath).name + SnackbarUtil.show(this, R.string.filedetails_renamed_in_upload_msg, newName) } - if (createdFolder == null) { - Log_OC.e( - TAG, - "onCreateFolderOperationFinish(): file not saved after create folder operation, cannot encrypt" - ) - return + if (uploadSucceeded || file?.fileExists() == true) { + fileDetailFragment.updateFileDetails(false, true) + } else { + onBackPressedDispatcher.onBackPressed() } - fileOperationsHelper.toggleEncryption(createdFolder, true) - } - /** - * {@inheritDoc} - */ - override fun onTransferStateChanged(file: OCFile, downloading: Boolean, uploading: Boolean) { - updateListOfFilesFragment() - val fileDetailFragment = leftFragment as? FileDetailFragment ?: return - if (file != fileDetailFragment.file) return - val currentUser = user.orElse(null) ?: return + if (uploadSucceeded) { + file?.let { previewUploadedFile(it) } + } + } + private fun previewUploadedFile(uploadedFile: OCFile) { when { - downloading || uploading -> fileDetailFragment.updateFileDetails(file, currentUser) - !file.fileExists() -> resetScrollingAndUpdateActionBar() - else -> fileDetailFragment.updateFileDetails(false, true) + PreviewMediaFragment.canBePreviewed(uploadedFile) -> startImagePreview(uploadedFile, true) + PreviewTextFileFragment.canBePreviewed(uploadedFile) -> startTextPreview(uploadedFile, true) } } - private fun requestForDownload() { - val currentUser = user.orElse(null) ?: return - waitingToPreview?.let { - FileDownloadHelper.instance().downloadFileIfNotStartedBefore(currentUser, it) + private fun refreshVisibleAlbumScreen() { + when { + isAlbumsFragment -> + (supportFragmentManager.findFragmentByTag(AlbumsFragment.TAG) as? AlbumsFragment)?.refreshAlbums() + + isAlbumItemsFragment -> + (supportFragmentManager.findFragmentByTag(AlbumItemsFragment.TAG) as? AlbumItemsFragment) + ?.refreshData() } } + // endregion + + // region Downloads + fun setFileIDForImmediatePreview(fileId: Long) { + immediatePreviewFileId = fileId + } - override fun onSavedCertificate() { - startSyncFolderOperation(getCurrentDir(), false) + private fun onDownloadStarted(intent: Intent) { + Log_OC.d(TAG, "download worker started") + getFileOfDownloadEvent(intent)?.let { updateFileDownloadIndicator(FileDownloadIndicator.Downloading, it) } + requestDownloadOfWaitingPreview() } - /** - * Starts an operation to refresh the requested folder. - * - * - * The operation is run in a new background thread created on the fly. - * - * - * The refresh updates is a "light sync": properties of regular files in folder are updated (including associated - * shares), but not their contents. Only the contents of files marked to be kept-in-sync are synchronized too. - * - * @param folder Folder to refresh. - * @param ignoreETag If 'true', the data from the server will be fetched and sync'ed even if the eTag didn't - * change. - * @param ignoreFocus reloads file list even without focus, e.g. on tablet mode, focus can still be in detail view - */ - @JvmOverloads - fun startSyncFolderOperation(folder: OCFile?, ignoreETag: Boolean, ignoreFocus: Boolean = false) { - Log_OC.d(TAG, "startSyncFolderOperation called, ignoreEtag: $ignoreETag, ignoreFocus: $ignoreFocus") + private fun onDownloadCompleted(intent: Intent) { + Log_OC.d(TAG, "file download completed received") + getFileOfDownloadEvent(intent)?.let { updateFileDownloadIndicator(FileDownloadIndicator.Downloaded, it) } - if (!searchQuery.isNullOrEmpty() || user.isEmpty) { + if (immediatePreviewFileId == NO_IMMEDIATE_PREVIEW_FILE_ID) { + Log_OC.d(TAG, "updating ui for file download") + updateUiAfterDownload(intent) return } - val syncFolder = Runnable { executeSyncFolderOperation(folder, ignoreETag) } + Log_OC.d(TAG, "updating ui immediate file preview") - // The refresh must not run while another window floats over the activity, e.g. a dialog that is being - // dismissed or a rotation. Rather than waiting a fixed delay run right away when it already has focus - // and replay the request on the next focus gain. - if (ignoreFocus || hasWindowFocus()) { - pendingSyncFolderOperation = null - syncFolder.run() + val downloadedRemotePath = intent.getStringExtra(FileDownloadEventBroadcaster.EXTRA_REMOTE_PATH) + val downloadedFile = storageManager.getFileByDecryptedRemotePath(downloadedRemotePath) ?: return + if (immediatePreviewFileId != downloadedFile.fileId || !downloadedFile.isDown) return + + immediatePreviewFileId = NO_IMMEDIATE_PREVIEW_FILE_ID + if (PreviewMediaFragment.canBePreviewed(downloadedFile)) { + startImagePreview(downloadedFile, downloadedFile.isDown) } else { - pendingSyncFolderOperation = syncFolder + previewFile(downloadedFile, null) } } - override fun onWindowFocusChanged(hasFocus: Boolean) { - super.onWindowFocusChanged(hasFocus) + private fun updateUiAfterDownload(intent: Intent) { + val downloadedRemotePath = intent.getStringExtra(FileDownloadEventBroadcaster.EXTRA_REMOTE_PATH) + val downloadBehaviour = intent.getStringExtra(FileDownloadEventBroadcaster.EXTRA_DOWNLOAD_BEHAVIOUR) + val accountName = intent.getStringExtra(FileDownloadEventBroadcaster.EXTRA_ACCOUNT_NAME) - if (!hasFocus) { - return + if (isCurrentAccount(accountName) && isInCurrentDir(downloadedRemotePath)) { + updateListOfFilesFragment() + val downloadEvent = intent.action + if (downloadEvent != null && downloadedRemotePath != null) { + refreshDetailsFragmentIfVisible( + downloadEvent, + downloadedRemotePath, + intent.getBooleanExtra(FileDownloadEventBroadcaster.EXTRA_DOWNLOAD_RESULT, false) + ) + } } - pendingSyncFolderOperation?.let { - pendingSyncFolderOperation = null - it.run() + waitingToSend?.let { previousFile -> + val refreshedFile = storageManager.getFileByRemoteId(previousFile.remoteId) + waitingToSend = refreshedFile + if (refreshedFile?.isDown == true && OCFileListFragment.DOWNLOAD_SEND == downloadBehaviour) { + val packageName = intent.getStringExtra(FileDownloadEventBroadcaster.EXTRA_PACKAGE_NAME) ?: return + val activityName = intent.getStringExtra(FileDownloadEventBroadcaster.EXTRA_ACTIVITY_NAME) ?: return + sendDownloadedFile(packageName, activityName) + } + } + + waitingToPreview?.let { previousFile -> + val refreshedFile = storageManager.getFileByRemoteId(previousFile.remoteId) + waitingToPreview = refreshedFile + if (refreshedFile?.isDown == true && EditImageActivity.OPEN_IMAGE_EDITOR == downloadBehaviour) { + startImageEditor(refreshedFile) + } } } - private fun executeSyncFolderOperation(folder: OCFile?, ignoreETag: Boolean) { - folder ?: return - val currentUser = user.orElse(null) ?: return + private fun getFileOfDownloadEvent(intent: Intent): OCFile? { + val remotePath = intent.getStringExtra(FileDownloadEventBroadcaster.EXTRA_REMOTE_PATH) + return fileDataStorageManager.getFileByDecryptedRemotePath(remotePath) + } - syncState = EmptyListState.LOADING + private fun updateFileDownloadIndicator(indicator: FileDownloadIndicator, file: OCFile) { + ocFileListFragment?.adapter?.updateFileIndicator(indicator.iconId, file) + } - RefreshFolderOperation( - folder, - System.currentTimeMillis(), - false, - ignoreETag, - storageManager, - currentUser, - applicationContext - ).execute( - account, - this, - { _, _ -> onSyncFinished() }, - handler, - null - ) + private fun onFolderFileDownloadProgress(fileId: Long, previousFileId: Long?) { + val adapter = ocFileListFragment?.adapter ?: return + previousFileId?.let { storageManager.getFileById(it) }?.let(adapter::replaceFile) + adapter.updateFileIndicator(fileId, FileDownloadIndicator.Downloading.iconId) + } - fetchRecommendedFilesIfNeeded(ignoreETag, folder) + protected fun refreshDetailsFragmentIfVisible( + downloadEvent: String, + downloadedRemotePath: String, + success: Boolean + ) { + val detailFragment = leftFragment as? FileDetailFragment ?: return + val fileInFragment = detailFragment.file + + if (fileInFragment != null && downloadedRemotePath != fileInFragment.remotePath) { + waitingToPreview = null + return + } + + when (downloadEvent) { + FileDownloadEventBroadcaster.ACTION_DOWNLOAD_ENQUEUED -> { + detailFragment.listenForTransferProgress() + detailFragment.updateFileDetails(true, false) + } + + FileDownloadEventBroadcaster.ACTION_DOWNLOAD_COMPLETED -> { + val isWaitedPreview = waitingToPreview?.remotePath == downloadedRemotePath + val previewStarted = isWaitedPreview && startPreviewOfWaitingFile(success) + waitingToPreview = null + if (!previewStarted) { + detailFragment.updateFileDetails(false, success) + } + } + } } - private fun fetchRecommendedFilesIfNeeded(ignoreETag: Boolean, folder: OCFile?) { - val capability = capabilities.orElse(null) ?: return - if (folder?.isRootDirectory == false || capability.recommendations.isFalse) return + private fun startPreviewOfWaitingFile(downloadSucceeded: Boolean): Boolean { + if (!downloadSucceeded) return false - val accountName = user.orElse(null)?.accountName ?: return - val fragment = listOfFilesFragment - lifecycleScope.launch { - val recommendedFiles = withContext(Dispatchers.IO) { - filesRepository.fetchRecommendedFiles(accountName, ignoreETag, storageManager) + waitingToPreview = waitingToPreview?.let { storageManager.getFileById(it.fileId) } + val fileToPreview = waitingToPreview ?: return false + + return when { + MimeTypeUtil.isVideo(fileToPreview) -> { + startImagePreview(fileToPreview, true) + true + } + + canPreviewInAudioPlayer(fileToPreview) -> { + startAudioPreview(fileToPreview, true, true) + true + } + + MimeTypeUtil.isVCard(fileToPreview.mimeType) -> { + startContactListFragment(fileToPreview) + true + } + + PreviewTextFileFragment.canBePreviewed(fileToPreview) -> { + startTextPreview(fileToPreview, true) + true + } + + MimeTypeUtil.isPDF(fileToPreview) -> { + startPdfPreview(fileToPreview) + true + } + + else -> { + fileOperationsHelper.openFile(fileToPreview) + false } - fragment?.adapter?.updateRecommendedFiles(recommendedFiles) } } - private fun requestForDownload(file: OCFile, downloadBehaviour: String, packageName: String, activityName: String) { + private fun requestDownloadOfWaitingPreview() { + val previousFile = waitingToPreview ?: return + if (storageManager == null) return + + val refreshedFile = storageManager.getFileById(previousFile.fileId) + waitingToPreview = refreshedFile + if (refreshedFile?.isDown == false) { + downloadWaitingPreview() + } + } + + private fun downloadWaitingPreview() { + val currentUser = user.orElse(null) ?: return + waitingToPreview?.let { + FileDownloadHelper.instance().downloadFileIfNotStartedBefore(currentUser, it) + } + } + + private fun downloadFileIfNotDownloading( + file: OCFile, + downloadBehaviour: String, + packageName: String, + activityName: String + ) { val currentUser = user.orElse(null) ?: return val downloadHelper = FileDownloadHelper.instance() if (downloadHelper.isDownloading(currentUser, file)) return @@ -2464,6 +1993,48 @@ class FileDisplayActivity : ) } + override fun newTransferenceServiceConnection(): ServiceConnection = DownloadServiceConnection(this) + + override fun onTransferStateChanged(file: OCFile, downloading: Boolean, uploading: Boolean) { + updateListOfFilesFragment() + val fileDetailFragment = leftFragment as? FileDetailFragment ?: return + if (file != fileDetailFragment.file) return + val currentUser = user.orElse(null) ?: return + + when { + downloading || uploading -> fileDetailFragment.updateFileDetails(file, currentUser) + !file.fileExists() -> resetScrollingAndUpdateActionBar() + else -> fileDetailFragment.updateFileDetails(false, true) + } + } + + fun cancelTransference(file: OCFile) { + fileOperationsHelper.cancelTransference(file) + if (waitingToPreview?.remotePath == file.remotePath) { + waitingToPreview = null + } + if (waitingToSend?.remotePath == file.remotePath) { + waitingToSend = null + } + onTransferStateChanged(file, false, false) + } + + fun cancelTransference(files: MutableCollection) { + files.forEach { cancelTransference(it) } + } + + override fun downloadFile(file: OCFile, packageName: String, activityName: String) { + startDownloadForSending(file, OCFileListFragment.DOWNLOAD_SEND, packageName, activityName) + } + + /** + * Downloads the file and shares it with the given app once the download finishes. + */ + fun startDownloadForSending(file: OCFile, downloadBehaviour: String, packageName: String, activityName: String) { + waitingToSend = file + downloadFileIfNotDownloading(file, downloadBehaviour, packageName, activityName) + } + private fun sendDownloadedFile(packageName: String, activityName: String) { val fileToSend = waitingToSend ?: run { Log_OC.e(TAG, "Trying to send a NULL OCFile") @@ -2480,568 +2051,531 @@ class FileDisplayActivity : } /** - * Requests the download of the received [OCFile] , updates the UI to monitor the download progress and - * prepares the activity to send the file when the download finishes. + * Downloads the file and previews or opens it once the download finishes. * - * @param file [OCFile] to download and preview. - * @param packageName - * @param activityName + * @param parentFolder folder containing [file] */ - fun startDownloadForSending(file: OCFile?, downloadBehaviour: String, packageName: String, activityName: String) { - waitingToSend = file - file?.let { requestForDownload(it, downloadBehaviour, packageName, activityName) } + fun startDownloadForPreview(file: OCFile, parentFolder: OCFile?) { + if (!file.isFileEligibleForImmediatePreview) { + user.orElse(null)?.let { + replaceLeftFragment(FileDetailFragment.newInstance(file, parentFolder, it), showSortListGroup = false) + } + } + + configureToolbarForPreview(file) + waitingToPreview = file + downloadWaitingPreview() + setFile(file) } + // endregion - fun startImagePreview( - file: OCFile, - showPreview: Boolean, - type: VirtualFolderType? = null, - mediaState: MediaState? = null, - sourceView: View? = null - ) { - val intent = imagePreviewIntent(file, type, mediaState) ?: return + // region Remote operations + override fun onRemoteOperationFinish(operation: RemoteOperation<*>?, result: RemoteOperationResult<*>) { + super.onRemoteOperationFinish(operation, result) - if (showPreview) { - NavigationAnimator(this).scaleUp(intent, sourceView) - } else { - startSyncForFileAndIntent(file, intent) - } - } + when (operation) { + is RemoveFileOperation -> onRemoveFileOperationFinish(operation, result) - private fun startSyncForFileAndIntent(file: OCFile?, intent: Intent) { - FileOperationsHelper(this, userAccountManager, connectivityService, editorUtils) - .startSyncForFileAndIntent(file, intent) - } + is RenameFileOperation -> onRenameFileOperationFinish(operation, result) - private fun previewIntentFor(file: OCFile?, previewKey: String): Intent = Intent().apply { - putExtra(EXTRA_FILE, file) - putExtra(previewKey, true) - } + is SynchronizeFileOperation -> onSynchronizeFileOperationFinish(operation, result) - private fun imagePreviewIntent(file: OCFile, type: VirtualFolderType?, mediaState: MediaState?): Intent? { - val currentUser = user.orElse(null) ?: run { - Log_OC.e(TAG, "cannot start image preview") - return null - } + is CreateFolderOperation -> onCreateFolderOperationFinish(operation, result) - return Intent(this, PreviewMediaActivity::class.java).apply { - putExtra(EXTRA_FILE, file) - putExtra(EXTRA_LIVE_PHOTO_FILE, file.livePhotoVideo) - putExtra(EXTRA_USER, currentUser) - type?.let { putExtra(PreviewMediaActivity.EXTRA_VIRTUAL_TYPE, it) } - mediaState?.let { putExtra(PreviewMediaActivity.EXTRA_MEDIA_STATE, it) } - } - } + is MoveFileOperation -> onMoveFileOperationFinish(operation, result) - /** - * Starts the preview of an audio [OCFile], synchronizing it first when it is not available yet. - */ - fun startAudioPreview(file: OCFile, showPreview: Boolean, streamMedia: Boolean) { - // not reachable under normal conditions - if (user.isEmpty) return + is CopyFileOperation -> onCopyFileOperationFinish(operation, result) - if ((showPreview && file.isDown && !file.isDownloading) || streamMedia) { - startAudioPlayer(file) - } else { - startSyncForFileAndIntent(file, previewIntentFor(file, AUDIO_PREVIEW)) - } - } + is RestoreFileVersionRemoteOperation -> onRestoreFileVersionOperationFinish(result) - private fun startAudioPlayer(file: OCFile) { - val collection = listOfFilesFragment - ?.takeIf { it.isSearchFragment } - ?.currentSearchType - .toPlaybackCollection() - audioPlayerLauncher.launch(this, file, collection) - } + is CreateNewAlbumRemoteOperation -> albumOperationListener.onCreateAlbumOperationFinish(operation, result) - fun configureToolbarForPreview(file: OCFile?) { - lockScrolling() - updateActionBarForFile(file) - } + is CopyFileToAlbumOperation -> albumOperationListener.onCopyAlbumFileOperationFinish(operation, result) - /** - * Starts the preview of a text file [OCFile]. - * - * @param file Text [OCFile] to preview. - */ - fun startTextPreview(file: OCFile?, showPreview: Boolean) { - // remnants of old unsafe system; do not crash, silently stop - val currentUser = user.orElse(null) ?: return + is RenameAlbumRemoteOperation -> albumOperationListener.onRenameAlbumOperationFinish(operation, result) - if (showPreview) { - val fragment = PreviewTextFileFragment.create(currentUser, file, searchOpen, searchQuery) - setLeftFragment(fragment, false) - configureToolbarForPreview(file) - showBottomNavigationBar(false) - } else { - startSyncForFileAndIntent(file, previewIntentFor(file, TEXT_PREVIEW)) - } - } + is RemoveAlbumRemoteOperation -> albumOperationListener.onRemoveAlbumOperationFinish(operation, result) - /** - * Starts rich workspace preview for a folder. - * - * @param folder [OCFile] to preview its rich workspace. - */ - fun startRichWorkspacePreview(folder: OCFile?) { - configureToolbarForPreview(folder) - val textPreviewFragment = PreviewTextStringFragment().apply { - arguments = Bundle().apply { putParcelable(EXTRA_FILE, folder) } + is PublicShareLinkAlbumRemoteOperation -> + albumOperationListener.onAlbumPublicLinkOperationFinish(operation, result) } - setLeftFragment(textPreviewFragment, false) } - fun startContactListFragment(file: OCFile?) { - val currentUser = user.orElse(null) ?: return - ContactsPreferenceActivity.startActivityWithContactsFile(this, currentUser, file) - } + private fun onRemoveFileOperationFinish(operation: RemoveFileOperation, result: RemoteOperationResult<*>) { + deleteBatchTracker.onSingleDeleteFinished() + if (!result.isSuccess && result.isSslRecoverableException) { + showUntrustedCertDialogFor(result) + return + } - fun startPdfPreview(file: OCFile) { - // prefer third party PDF apps - if (fileOperationsHelper.canOpenFile(file)) { - fileOperationsHelper.openFile(file) + if (!result.isSuccess) { + Log_OC.e(TAG, "deletion failed") return } - setLeftFragment(PreviewPdfFragment.newInstance(file), false) - configureToolbarForPreview(file) - setMainFabVisible(false) - } + val removedFile = operation.file + file?.let { playbackModel.stopPlaying(it) } + val currentLeftFragment = leftFragment - /** - * Requests the download of the received [OCFile] , updates the UI to monitor the download progress and - * prepares the activity to preview or open the file when the download finishes. - * - * @param file [OCFile] to download and preview. - * @param parentFolder [OCFile] containing above file - */ - fun startDownloadForPreview(file: OCFile, parentFolder: OCFile?) { - if (!file.isFileEligibleForImmediatePreview) { - user.orElse(null)?.let { - setLeftFragment(FileDetailFragment.newInstance(file, parentFolder, it), false) - } + val isRemovedFileStillStored = storageManager.fileExists(removedFile.fileId) + if (currentLeftFragment is FileFragment && + !isRemovedFileStillStored && + removedFile == currentLeftFragment.file + ) { + file = storageManager.getFileById(removedFile.parentId) + resetScrollingAndUpdateActionBar() } - configureToolbarForPreview(file) - waitingToPreview = file - requestForDownload() - setFile(file) + if (currentLeftFragment is OCFileListFragment) { + updateFileListAfterRemoval(currentLeftFragment, removedFile, operation.onlyLocalCopy) + } + + supportInvalidateOptionsMenu() + fetchRecommendedFilesIfNeeded(ignoreETag = true, currentDir) } - /** - * Opens EditImageActivity with given file loaded. If file is not available locally, it will be synced before - * opening the image editor. - * - * @param file [OCFile] (image) to be loaded into image editor - */ - fun startImageEditor(file: OCFile) { - if (!file.isDown) { - waitingToPreview = file - requestForDownload(file, EditImageActivity.OPEN_IMAGE_EDITOR, packageName, javaClass.simpleName) + private fun updateFileListAfterRemoval(fragment: OCFileListFragment, removedFile: OCFile, onlyLocalCopy: Boolean) { + val adapter = fragment.adapter ?: return + if (onlyLocalCopy) { + adapter.notifyItemChanged(removedFile) return } - val editImageIntent = Intent(this, EditImageActivity::class.java).apply { - putExtra(EditImageActivity.EXTRA_FILE, file) + adapter.removeFile(removedFile) + if (adapter.isEmpty) { + val emptyState = fragment.searchEvent?.toSearchType() ?: SearchType.NO_SEARCH + fragment.setEmptyListMessage(emptyState) } - startActivity(editImageIntent) } - /** - * Request stopping the upload/download operation in progress over the given [OCFile] file. - * - * @param file [OCFile] file which operation are wanted to be cancel - */ - fun cancelTransference(file: OCFile) { - fileOperationsHelper.cancelTransference(file) - if (waitingToPreview?.remotePath == file.remotePath) { - waitingToPreview = null - } - if (waitingToSend?.remotePath == file.remotePath) { - waitingToSend = null + override fun onAutoUploadFolderRemoved( + entities: List, + filesToRemove: List, + onlyLocalCopy: Boolean + ) { + dialogs.showAutoUploadFolderDeleteConfirmation { + removeAutoUploadFolders(entities, filesToRemove, onlyLocalCopy) } - onTransferStateChanged(file, false, false) } - /** - * Request stopping all upload/download operations in progress over the given [OCFile] files. - * - * @param files collection of [OCFile] files which operations are wanted to be cancel - */ - fun cancelTransference(files: MutableCollection) { - files.forEach { cancelTransference(it) } - } + private fun removeAutoUploadFolders( + entities: List, + filesToRemove: List, + onlyLocalCopy: Boolean + ) { + lifecycleScope.launch(Dispatchers.IO) { + entities.mapNotNull { it.id?.toLong() }.forEach { + fileUploadHelper.removeEntityFromUploadEntities(it) + syncedFolderProvider.deleteSyncedFolder(it) + } - override fun onRefresh(ignoreETag: Boolean) { - syncAndUpdateFolder(ignoreETag, ignoreFocus = false) + connectivityService.isNetworkAndServerAvailable { isAvailable -> + when { + isAvailable -> fileOperationsHelper?.removeFiles(filesToRemove, onlyLocalCopy, true) + onlyLocalCopy -> fileOperationsHelper?.removeFiles(filesToRemove, true, true) + else -> filesToRemove.forEach { fileDataStorageManager.addRemoveFileOfflineOperation(it) } + } + onFilesRemoved() + } + } } - override fun onRefresh() { - syncAndUpdateFolder(ignoreETag = true, ignoreFocus = false) - } + private fun onRestoreFileVersionOperationFinish(result: RemoteOperationResult<*>) { + if (!result.isSuccess) { + SnackbarUtil.show(this, R.string.file_version_restored_error) + return + } - private fun syncAndUpdateFolder(ignoreETag: Boolean, ignoreFocus: Boolean) { - val listOfFiles = listOfFilesFragment ?: return - if (listOfFiles.isSearchFragment) return + val restoredFile = getFile() - val folder = listOfFiles.currentFile ?: return - startSyncFolderOperation(folder, ignoreETag, ignoreFocus) - } + // the new version is only downloaded when the old one was downloaded + if (restoredFile?.isDown == true) { + fileOperationsHelper.removeFiles(listOf(restoredFile), true, true) + showSyncLoadingDialog(restoredFile.isFolder) + fileOperationsHelper.syncFileOrFolder(restoredFile) + } - override fun showFiles(onDeviceOnly: Boolean, personalFiles: Boolean) { - super.showFiles(onDeviceOnly, personalFiles) - refreshOrInitOCFileListFragment() + val parent = restoredFile?.let { storageManager.getFileById(it.parentId) } + startSyncFolderOperation(parent, ignoreETag = true, ignoreFocus = true) + + (leftFragment as? FileDetailFragment)?.fileDetailActivitiesFragment?.reload() } - private fun refreshOrInitOCFileListFragment() { - val ocFileListFragment = listOfFilesFragment - if (ocFileListFragment != null && - ocFileListFragment !is GalleryFragment && - ocFileListFragment !is SharedListFragment && - // album fragment check will help in showing offline files screen - // when navigating from Albums to Offline Files - !isAlbumsFragment && - !isAlbumItemsFragment - ) { - ocFileListFragment.refreshDirectory() - } else { - leftFragment = OCFileListFragment() + private fun onMoveFileOperationFinish(operation: MoveFileOperation, result: RemoteOperationResult<*>) { + if (!result.isSuccess) { + showErrorCauseMessage(result, operation) } } - @Subscribe(threadMode = ThreadMode.BACKGROUND) - fun onMessageEvent(event: SearchEvent) { - when (event.searchType) { - SearchRemoteOperation.SearchType.PHOTO_SEARCH -> { - Log_OC.d(this, "Switch to photo search fragment") - leftFragment = GalleryFragment() - } - - SearchRemoteOperation.SearchType.SHARED_FILTER -> { - Log_OC.d(this, "Switch to Shared fragment") - leftFragment = SharedListFragment() - } - - else -> Unit + private fun onCopyFileOperationFinish(operation: CopyFileOperation, result: RemoteOperationResult<*>) { + if (!result.isSuccess) { + showErrorCauseMessage(result, operation) + return } - listOfFilesFragment?.updateSearchType(event) - updateActionBarTitleAndHomeButton(null) + updateListOfFilesFragment() + refreshGalleryFragmentIfNeeded() } - @Subscribe(threadMode = ThreadMode.MAIN) - fun onMessageEvent(event: SyncEventFinished) { - val intent = event.intent - val bundle = intent.extras ?: return - val file = bundle.getParcelableArgument(EXTRA_FILE, OCFile::class.java) ?: return + private fun onRenameFileOperationFinish(operation: RenameFileOperation, result: RemoteOperationResult<*>) { + val currentUser = user.orElse(null) + val renamedFile = operation.file + if (!result.isSuccess || currentUser == null) { + SnackbarUtil.show(this, ErrorMessageAdapter.getErrorCauseMessage(result, operation, resources)) - when { - intent.getBooleanExtra(TEXT_PREVIEW, false) -> startTextPreview(file, true) + if (result.isSslRecoverableException) { + showUntrustedCertDialogFor(result) + } + return + } - intent.getBooleanExtra(AUDIO_PREVIEW, false) -> startAudioPreview(file, true, true) + (leftFragment as? FileFragment)?.let { refreshFileFragmentAfterRename(it, renamedFile, currentUser) } - bundle.containsKey(PreviewMediaActivity.EXTRA_VIRTUAL_TYPE) -> startImagePreview( - file, - true, - bundle.getSerializableArgument(PreviewMediaActivity.EXTRA_VIRTUAL_TYPE, VirtualFolderType::class.java), - bundle.getSerializableArgument(PreviewMediaActivity.EXTRA_MEDIA_STATE, MediaState::class.java), - galleryThumbnailOf(file) - ) + val parentDir = storageManager.getFileById(renamedFile.parentId) + val isRenamedInCurrentDir = parentDir != null && parentDir == currentDir + + // the parent check fails in the Shared/Favorites root, so the root is always refreshed too + if (currentDir?.isRootDirectory == true || isRenamedInCurrentDir) { + val fileList = topFileListFragment - else -> startImagePreview(file, true, sourceView = galleryThumbnailOf(file)) + // OCFileSearchTask may still run during rename, so use a single refresh path to avoid + // flicker/inconsistent filenames. + if (fileList?.isSearchFragment == true) { + fileList.restartSearch() + } else { + fileList?.adapter?.updateFile(renamedFile) + } } + + refreshGalleryFragmentIfNeeded() + fetchRecommendedFilesIfNeeded(ignoreETag = true, currentDir) } - private fun detailsHeaderImage(): View? = - previewImageView?.takeIf { it.isShown && it.isLaidOut && it.drawable != null } + private fun refreshFileFragmentAfterRename(fragment: FileFragment, renamedFile: OCFile, user: User) { + if (fragment.file != renamedFile) return - private fun galleryThumbnailOf(file: OCFile): View? = leftFragment?.view - ?.findViewWithTag(file.fileId) - ?.takeIf { it.isShown && ViewCompat.getTransitionName(it) == NavigationAnimator.sharedElementName(file) } + when (fragment) { + is FileDetailFragment -> { + fragment.updateFileDetails(renamedFile, user) + showDetails(renamedFile) + } - @Subscribe(threadMode = ThreadMode.BACKGROUND) - fun onMessageEvent(event: TokenPushEvent?) { - if (preferences.isKeysReInitEnabled()) { - PushUtils.pushRegistrationToServer(userAccountManager, preferences.getPushToken()) - } else { - PushUtils.reinitKeys(userAccountManager) + is PreviewTextFileFragment -> { + fragment.updateFile(renamedFile) + if (PreviewTextFileFragment.canBePreviewed(renamedFile)) { + startTextPreview(renamedFile, true) + } else { + fileOperationsHelper.openFile(renamedFile) + } + } } } - public override fun onStart() { - super.onStart() - - registerReceivers() - - if (SettingsActivity.isBackPressed) { - Log_OC.d(TAG, "User returned from settings activity, skipping reset content logic") - return - } + private fun onSynchronizeFileOperationFinish( + operation: SynchronizeFileOperation, + result: RemoteOperationResult<*> + ) { + if (!result.isSuccess || !operation.transferWasRequested) return - initFile() + val syncedFile = operation.localFile ?: return + onTransferStateChanged(syncedFile, true, true) + supportInvalidateOptionsMenu() + refreshVisibleFileDetails() } - private fun initFile() { - val existingUser = user.orElse(null) ?: run { - Log_OC.w(TAG, "user is not available, cannot init file") + private fun onCreateFolderOperationFinish(operation: CreateFolderOperation, result: RemoteOperationResult<*>) { + if (!result.isSuccess) { + if (RemoteOperationResult.ResultCode.FOLDER_ALREADY_EXISTS == result.code) { + SnackbarUtil.show(this, R.string.folder_already_exists) + } else { + showErrorCauseMessage(result, operation) + } return } - val currentStorageManager: FileDataStorageManager = storageManager ?: run { - Log_OC.w(TAG, "storage manager is null, cannot init file") + val fileList = listOfFilesFragment + val createdFolder = storageManager.getFileByDecryptedRemotePath(operation.remotePath) + if (!operation.shouldEncrypt()) { + createdFolder?.let { fileList?.onItemClicked(it) } return } - val file = resolveInitialFile(currentStorageManager) ?: run { - Log_OC.e(TAG, "Could not retrieve root folder – cannot continue") + if (createdFolder == null) { + Log_OC.e( + TAG, + "onCreateFolderOperationFinish(): file not saved after create folder operation, cannot encrypt" + ) return } - - setFile(file) - - val existingAccountName = existingUser.accountName - mSwitchAccountButton.tag = existingAccountName - - avatarGenerator.setAccountAvatar( - existingUser, - this, - resources.getDimension(R.dimen.nav_drawer_menu_avatar_radius), - mSwitchAccountButton - ) - val userChanged = existingAccountName != lastDisplayedAccountName - if (userChanged) { - composeProcessTextAlias.configure() - Log_OC.d(TAG, "Initializing Fragments in onAccountChanged..") - initFragments() - if (file.isFolder && searchQuery.isNullOrEmpty()) { - startSyncFolderOperation(file, false) - } - } else { - updateActionBarTitleAndHomeButton(if (file.isFolder) null else file) - } - - setNewLastDisplayedAccountName(existingAccountName) - EventBus.getDefault().post(TokenPushEvent()) - checkForNewDevVersionNecessary(applicationContext) + fileOperationsHelper.toggleEncryption(createdFolder, true) } - private fun resolveInitialFile(storageManager: FileDataStorageManager): OCFile? = - getFile()?.let { resolveCurrentFile(it, storageManager) } ?: storageManager.getFileByPath(OCFile.ROOT_PATH) + private fun refreshVisibleFileDetails() { + val details = leftFragment as? FileFragment ?: return - private fun resolveCurrentFile(file: OCFile, storageManager: FileDataStorageManager): OCFile? { - if (!file.isDown || file.lastSyncDateForProperties != 0L) { - return storageManager.getFileByPath(file.remotePath) + details.file?.let { + val refreshedFile = storageManager.getFileByPath(it.remotePath) + if (details is PreviewTextFragment) { + (details as? PreviewTextFileFragment)?.updateFile(refreshedFile) + } else { + showDetails(refreshedFile) + } } - - val parentPathLength = file.remotePath.lastIndexOf(file.fileName) - if (parentPathLength <= 0) return null - - val parentPath = file.remotePath.take(parentPathLength) - return file.takeIf { storageManager.getFileByPath(parentPath) != null } + supportInvalidateOptionsMenu() } - private fun setNewLastDisplayedAccountName(accountName: String) { - preferences.lastDisplayedAccountName = accountName - lastDisplayedAccountName = accountName + private fun refreshGalleryFragmentIfNeeded() { + if (topFileListFragment is GalleryFragment) { + startPhotoSearch(R.id.nav_gallery) + } } - override fun onRestart() { - super.onRestart() - checkForNewDevVersionNecessary(applicationContext) + private fun showUntrustedCertDialogFor(result: RemoteOperationResult<*>) { + lastSslUntrustedServerResult = result + showUntrustedCertDialog(result) } - fun setSearchQuery(query: String?) { - searchQuery = query + private fun showErrorCauseMessage(result: RemoteOperationResult<*>, operation: RemoteOperation<*>) { + try { + SnackbarUtil.show(this, ErrorMessageAdapter.getErrorCauseMessage(result, operation, resources)) + } catch (e: Resources.NotFoundException) { + Log_OC.e(TAG, "Error while trying to show fail message ", e) + } } + // endregion - private fun handleOpenFileViaIntent(intent: Intent) { - SnackbarUtil.show(this, getString(R.string.retrieving_file)) + // region Previews + fun canPreviewInAudioPlayer(file: OCFile): Boolean = MimeTypeUtil.isAudio(file) - val userName = intent.getStringExtra(KEY_ACCOUNT) - val fileId = intent.getStringExtra(KEY_FILE_ID) - val filePath = intent.getStringExtra(KEY_FILE_PATH) + fun canPreviewInMediaPager(file: OCFile): Boolean = + PreviewMediaFragment.canBePreviewed(file) || MimeTypeUtil.isVideo(file) - val intentData = intent.data - if (userName == null && fileId == null && intentData != null) { - openDeepLink(intentData) - return + fun previewImageWithSearchContext( + file: OCFile, + searchFragment: Boolean, + currentSearchType: SearchType?, + sourceView: View? + ) { + val type = if (searchFragment) { + when (currentSearchType) { + SearchType.FAVORITE_SEARCH -> VirtualFolderType.FAVORITE + SearchType.GALLERY_SEARCH -> VirtualFolderType.GALLERY + else -> VirtualFolderType.NONE + } + } else { + null } - val optionalUser = if (userName == null) user else userAccountManager.getUser(userName) - val targetUser = optionalUser.orElse(null) ?: run { - SnackbarUtil.show(this, getString(R.string.associated_account_not_found)) - return + val mediaState = if (type == VirtualFolderType.GALLERY) { + (listOfFilesFragment as? GalleryFragment)?.currentMediaState + } else { + null } - when { - !fileId.isNullOrEmpty() -> openFile(targetUser, fileId) - !filePath.isNullOrEmpty() -> openFileByPath(targetUser, filePath) - else -> accountClicked(targetUser) - } + val showPreview = file.isDown || MimeTypeUtil.isVideo(file) + startImagePreview(file, showPreview, type, mediaState, sourceView) } - private fun openDeepLink(uri: Uri) { - val match = DeepLinkHandler(userAccountManager).parseDeepLink(uri) + fun previewFile(file: OCFile, setFabVisible: CompletionCallback?) { + if (!file.isDown) { + Log_OC.d(TAG, "File is not downloaded, cannot be previewed") + return + } when { - match == null -> handleDeepLink(uri) - match.users.isEmpty() -> SnackbarUtil.show(this, getString(R.string.associated_account_not_found)) - match.users.size == SINGLE_USER_SIZE -> openFile(match.users.first(), match.fileId) - else -> selectUserAndOpenFile(match.users, match.fileId) - } - } + MimeTypeUtil.isVCard(file) -> startContactListFragment(file) + + MimeTypeUtil.isPDF(file) -> startPdfPreview(file) - private fun selectUserAndOpenFile(users: List, fileId: String?) { - val userNames = users.map { it.accountName }.toTypedArray() - val builder = MaterialAlertDialogBuilder(this) - .setTitle(R.string.common_choose_account) - .setItems(userNames) { _, which -> - openFile(users[which], fileId) - showLoadingDialog(getString(R.string.retrieving_file)) + PreviewTextFileFragment.canBePreviewed(file) -> { + setFabVisible?.onComplete(false) + startTextPreview(file, false) } - viewThemeUtils.dialog.colorMaterialAlertDialogBackground(applicationContext, builder) + canPreviewInAudioPlayer(file) -> { + setFabVisible?.onComplete(false) + startAudioPreview(file, true, false) + } - val dialog = builder.create() - dismissLoadingDialog() - dialog.show() + else -> fileOperationsHelper.openFile(file) + } } - private fun openFile(user: User?, fileId: String?) { - setUser(user) + fun startImagePreview( + file: OCFile, + showPreview: Boolean, + type: VirtualFolderType? = null, + mediaState: MediaState? = null, + sourceView: View? = null + ) { + val intent = imagePreviewIntent(file, type, mediaState) ?: return - if (fileId == null) { - onFileRequestError(null) - return + if (showPreview) { + NavigationAnimator(this).scaleUp(intent, sourceView) + } else { + syncFileThenStart(file, intent) } + } - if (user == null) { - Log_OC.e(TAG, "cannot fetch remote file, user is null") - return + private fun imagePreviewIntent(file: OCFile, type: VirtualFolderType?, mediaState: MediaState?): Intent? { + val currentUser = user.orElse(null) ?: run { + Log_OC.e(TAG, "cannot start image preview") + return null } - val capability = capabilities.orElse(null) ?: run { - Log_OC.e(TAG, "cannot fetch remote file, capabilities is empty") - return + return Intent(this, PreviewMediaActivity::class.java).apply { + putExtra(EXTRA_FILE, file) + putExtra(EXTRA_LIVE_PHOTO_FILE, file.livePhotoVideo) + putExtra(EXTRA_USER, currentUser) + type?.let { putExtra(PreviewMediaActivity.EXTRA_VIRTUAL_TYPE, it) } + mediaState?.let { putExtra(PreviewMediaActivity.EXTRA_MEDIA_STATE, it) } } + } - val fileStorageManager = storageManager ?: FileDataStorageManager(user, contentResolver) - val weakContext = WeakReference(this) - val task = FetchRemoteFileTask(user, fileId, fileStorageManager, lifecycleScope, capability, weakContext) - task.run(showFile = { (ocFile, message) -> - ocFile?.let { file = it } - showFile(ocFile, message) - }) + private fun syncFileThenStart(file: OCFile, intent: Intent) { + FileOperationsHelper(this, userAccountManager, connectivityService, editorUtils) + .startSyncForFileAndIntent(file, intent) } - private fun openFileByPath(user: User, filepath: String?) { - setUser(user) + private fun syncedPreviewIntent(file: OCFile, previewKey: String): Intent = Intent().apply { + putExtra(EXTRA_FILE, file) + putExtra(previewKey, true) + } - if (filepath == null) { - onFileRequestError(null) - return - } + /** + * Plays the audio file, synchronizing it first when it is not available yet. + */ + fun startAudioPreview(file: OCFile, showPreview: Boolean, streamMedia: Boolean) { + if (user.isEmpty) return - val fileStorageManager = storageManager ?: FileDataStorageManager(user, contentResolver) - val client = try { - clientFactory.create(user) - } catch (_: CreationException) { - onFileRequestError(null) - return + if ((showPreview && file.isDown && !file.isDownloading) || streamMedia) { + startAudioPlayer(file) + } else { + syncFileThenStart(file, syncedPreviewIntent(file, AUDIO_PREVIEW)) } - - val getRemoteFileTask = GetRemoteFileTask(this, filepath, client, fileStorageManager, user) - asyncRunner.postQuickTask( - getRemoteFileTask, - { result: GetRemoteFileTask.Result -> onFileRequestResult(result) }, - { throwable: Throwable? -> onFileRequestError(throwable) } - ) } - private fun onFileRequestError(throwable: Throwable?) { - dismissLoadingDialog() - SnackbarUtil.show(this, getString(R.string.error_retrieving_file)) - Log_OC.e(TAG, "Requesting file from remote failed!", throwable) + private fun startAudioPlayer(file: OCFile) { + val collection = listOfFilesFragment + ?.takeIf { it.isSearchFragment } + ?.currentSearchType + .toPlaybackCollection() + audioPlayerLauncher.launch(this, file, collection) } - private fun onFileRequestResult(result: GetRemoteFileTask.Result) { - dismissLoadingDialog() - - file = result.file + fun startTextPreview(file: OCFile, showPreview: Boolean) { + val currentUser = user.orElse(null) ?: return - val fileFragment = OCFileListFragment() - leftFragment = fileFragment + if (showPreview) { + val fragment = PreviewTextFileFragment.create(currentUser, file, wasSearchOpen, searchQuery) + replaceLeftFragment(fragment, showSortListGroup = false) + configureToolbarForPreview(file) + showBottomNavigationBar(false) + } else { + syncFileThenStart(file, syncedPreviewIntent(file, TEXT_PREVIEW)) + } + } - supportFragmentManager.executePendingTransactions() + fun startRichWorkspacePreview(folder: OCFile) { + configureToolbarForPreview(folder) + val textPreviewFragment = PreviewTextStringFragment().apply { + arguments = Bundle().apply { putParcelable(EXTRA_FILE, folder) } + } + replaceLeftFragment(textPreviewFragment, showSortListGroup = false) + } - fileFragment.onItemClicked(result.file) + fun startContactListFragment(file: OCFile) { + val currentUser = user.orElse(null) ?: return + ContactsPreferenceActivity.startActivityWithContactsFile(this, currentUser, file) } - fun performUnifiedSearch(query: String, listOfHiddenFiles: ArrayList?) { - val unifiedSearchFragment = - UnifiedSearchFragment.newInstance( - query, - listOfHiddenFiles, - currentDir?.decryptedRemotePath - ) - setLeftFragment(unifiedSearchFragment, false) + fun startPdfPreview(file: OCFile) { + // prefer third party PDF apps + if (fileOperationsHelper.canOpenFile(file)) { + fileOperationsHelper.openFile(file) + return + } + + replaceLeftFragment(PreviewPdfFragment.newInstance(file), showSortListGroup = false) + configureToolbarForPreview(file) + setMainFabVisible(false) } - fun setMainFabVisible(visible: Boolean) { - binding.fabMain.isVisible = visible + /** + * Opens the image editor, downloading the image first when it is not available locally. + */ + fun startImageEditor(file: OCFile) { + if (!file.isDown) { + waitingToPreview = file + downloadFileIfNotDownloading(file, EditImageActivity.OPEN_IMAGE_EDITOR, packageName, javaClass.simpleName) + return + } + + val editImageIntent = Intent(this, EditImageActivity::class.java).apply { + putExtra(EditImageActivity.EXTRA_FILE, file) + } + startActivity(editImageIntent) } - fun showFile(selectedFile: OCFile?, message: String?) { - getOCFileListFragmentFromFile { fragment -> - dismissLoadingDialog() + private fun detailsHeaderImage(): View? = + previewImageView?.takeIf { it.isShown && it.isLaidOut && it.drawable != null } - if (message.isNullOrEmpty()) { - val current = getCurrentDir() - fragment.listDirectory(current, file, MainApp.isOnlyOnDevice()) - file = current - updateActionBarTitleAndHomeButton(null) - } else { - fragment.view?.let { SnackbarUtil.show(it, message) } + private fun galleryThumbnailOf(file: OCFile): View? = leftFragment?.view + ?.findViewWithTag(file.fileId) + ?.takeIf { it.isShown && ViewCompat.getTransitionName(it) == NavigationAnimator.sharedElementName(file) } + // endregion + + // region EventBus + @Subscribe(threadMode = ThreadMode.BACKGROUND) + fun onMessageEvent(event: SearchEvent) { + when (event.searchType) { + SearchRemoteOperation.SearchType.PHOTO_SEARCH -> { + Log_OC.d(this, "Switch to photo search fragment") + replaceLeftFragment(GalleryFragment()) } - selectedFile?.let(fragment::onItemClicked) + SearchRemoteOperation.SearchType.SHARED_FILTER -> { + Log_OC.d(this, "Switch to Shared fragment") + replaceLeftFragment(SharedListFragment()) + } + + else -> Unit } - } - override fun onFilesRemoved() { - refreshCurrentDirectory() + listOfFilesFragment?.updateSearchType(event) + updateActionBarTitleAndHomeButton(null) } - private fun handleEcosystemIntent(intent: Intent?) { - ecosystemManager.receiveAccount( - intent, - object : AccountReceiverCallback { - override fun onAccountReceived(accountName: String) { - val receivedUser = accountManager.getUser(accountName).orElse(null) ?: run { - Log_OC.w(TAG, "user is not present") - SnackbarUtil.show(this@FileDisplayActivity, R.string.account_not_found) - return - } + @Subscribe(threadMode = ThreadMode.MAIN) + fun onMessageEvent(event: SyncEventFinished) { + val intent = event.intent + val extras = intent.extras ?: return + val syncedFile = extras.getParcelableArgument(EXTRA_FILE, OCFile::class.java) ?: return - accountClicked(receivedUser) - } + when { + intent.getBooleanExtra(TEXT_PREVIEW, false) -> startTextPreview(syncedFile, true) - override fun onAccountError(reason: String) { - Log_OC.w(TAG, "handleEcosystemIntent: $reason") - } - } - ) - } + intent.getBooleanExtra(AUDIO_PREVIEW, false) -> startAudioPreview(syncedFile, true, true) - // region MetadataSyncJob - private fun startMetadataSyncForRoot() { - backgroundJobManager.startMetadataSyncJob(OCFile.ROOT_PATH, folderAlreadySynced = true) + extras.containsKey(PreviewMediaActivity.EXTRA_VIRTUAL_TYPE) -> startImagePreview( + syncedFile, + true, + extras.getSerializableArgument(PreviewMediaActivity.EXTRA_VIRTUAL_TYPE, VirtualFolderType::class.java), + extras.getSerializableArgument(PreviewMediaActivity.EXTRA_MEDIA_STATE, MediaState::class.java), + galleryThumbnailOf(syncedFile) + ) + + else -> startImagePreview(syncedFile, true, sourceView = galleryThumbnailOf(syncedFile)) + } } - private fun startMetadataSyncForCurrentDir() { - val currentDirId = file?.decryptedRemotePath ?: return - backgroundJobManager.startMetadataSyncJob(currentDirId) + @Subscribe(threadMode = ThreadMode.BACKGROUND) + fun onMessageEvent(event: TokenPushEvent) { + if (preferences.isKeysReInitEnabled()) { + PushUtils.pushRegistrationToServer(userAccountManager, preferences.getPushToken()) + } else { + PushUtils.reinitKeys(userAccountManager) + } } // endregion @@ -3064,13 +2598,11 @@ class FileDisplayActivity : private const val KEY_WAITING_TO_PREVIEW = "WAITING_TO_PREVIEW" private const val KEY_SYNC_STATE = "SYNC_STATE" private const val KEY_WAITING_TO_SEND = "WAITING_TO_SEND" - private const val DIALOG_TAG_SHOW_TOS = "DIALOG_TAG_SHOW_TOS" private const val ON_RESUMED_RESET_DELAY = 10000L private const val SEARCH_VIEW_FOCUS_DELAY = 100L private const val NO_IMMEDIATE_PREVIEW_FILE_ID = -1L private const val UNKNOWN_VISIBILITY = -1 - private const val NO_NEUTRAL_BUTTON = -1 const val ACTION_DETAILS = "com.owncloud.android.ui.activity.action.DETAILS" const val EXTRA_RETURN_TO_PREVIEW = "RETURN_TO_PREVIEW" diff --git a/app/src/main/java/com/owncloud/android/ui/activity/filedisplayactivity/DownloadServiceConnection.kt b/app/src/main/java/com/owncloud/android/ui/activity/filedisplayactivity/DownloadServiceConnection.kt new file mode 100644 index 000000000000..a7ed39e867a6 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/filedisplayactivity/DownloadServiceConnection.kt @@ -0,0 +1,28 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.activity.filedisplayactivity + +import android.content.ComponentName +import android.content.Context +import android.content.ServiceConnection +import android.os.IBinder +import com.nextcloud.client.jobs.download.FileDownloadWorker +import com.owncloud.android.lib.common.utils.Log_OC + +class DownloadServiceConnection(private val context: Context) : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) = Unit + + override fun onServiceDisconnected(component: ComponentName) { + if (component == ComponentName(context, FileDownloadWorker::class.java)) { + Log_OC.d(TAG, "Download service disconnected") + } + } + + companion object { + private const val TAG = "DownloadServiceConnection" + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/filedisplayactivity/FileDisplayDialogs.kt b/app/src/main/java/com/owncloud/android/ui/activity/filedisplayactivity/FileDisplayDialogs.kt new file mode 100644 index 000000000000..191f6cd6e26d --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/filedisplayactivity/FileDisplayDialogs.kt @@ -0,0 +1,96 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.activity.filedisplayactivity + +import android.view.WindowManager.BadTokenException +import androidx.appcompat.app.AppCompatActivity +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.nextcloud.client.account.User +import com.nextcloud.utils.extensions.isDialogFragmentReady +import com.owncloud.android.R +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.ui.dialog.ConfirmationDialogFragment +import com.owncloud.android.ui.dialog.TermsOfServiceDialog +import com.owncloud.android.utils.theme.ViewThemeUtils + +class FileDisplayDialogs(private val activity: AppCompatActivity, private val viewThemeUtils: ViewThemeUtils) { + + fun showWrongStoragePath() { + try { + val builder = MaterialAlertDialogBuilder(activity, R.style.Theme_ownCloud_Dialog) + .setTitle(R.string.wrong_storage_path) + .setMessage(R.string.wrong_storage_path_desc) + .setPositiveButton(R.string.dialog_close) { dialog, _ -> dialog?.dismiss() } + .setIcon(R.drawable.ic_settings) + + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(activity.applicationContext, builder) + + builder.create().show() + } catch (e: BadTokenException) { + Log_OC.e(TAG, "Error showing wrong storage info, so skipping it: ${e.message}") + } + } + + fun showSyncedFoldersMigrationInfo(onOpenAutoUpload: () -> Unit) { + MaterialAlertDialogBuilder(activity, R.style.Theme_ownCloud_Dialog) + .setTitle(R.string.drawer_synced_folders) + .setMessage(R.string.synced_folders_new_info) + .setPositiveButton(R.string.drawer_open) { dialog, _ -> + dialog?.dismiss() + onOpenAutoUpload() + } + .setNegativeButton(R.string.drawer_close) { dialog, _ -> dialog?.dismiss() } + .setIcon(R.drawable.nav_synced_folders) + .show() + } + + fun showAccountChooser(users: List, onUserChosen: (User) -> Unit) { + val accountNames = users.map { it.accountName }.toTypedArray() + val builder = MaterialAlertDialogBuilder(activity) + .setTitle(R.string.common_choose_account) + .setItems(accountNames) { _, which -> onUserChosen(users[which]) } + + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(activity.applicationContext, builder) + + builder.create().show() + } + + fun showAutoUploadFolderDeleteConfirmation(onConfirm: () -> Unit) { + val dialog = ConfirmationDialogFragment.newInstance( + messageResId = R.string.auto_upload_delete_dialog_description, + messageArguments = null, + titleResId = R.string.auto_upload_delete_dialog_title, + titleIconId = R.drawable.ic_info, + positiveButtonTextId = R.string.common_delete, + negativeButtonTextId = R.string.common_cancel, + neutralButtonTextId = NO_NEUTRAL_BUTTON + ) + + dialog.setOnConfirmationListener(object : ConfirmationDialogFragment.ConfirmationDialogFragmentListener { + override fun onConfirmation(callerTag: String?) = onConfirm() + override fun onNeutral(callerTag: String?) = Unit + override fun onCancel(callerTag: String?) = Unit + }) + + if (activity.isDialogFragmentReady(dialog)) { + dialog.show(activity.supportFragmentManager, null) + } + } + + fun showTermsOfServiceIfNotShown() { + val fragmentManager = activity.supportFragmentManager + if (fragmentManager.findFragmentByTag(TERMS_OF_SERVICE_TAG) != null) return + + TermsOfServiceDialog().show(fragmentManager, TERMS_OF_SERVICE_TAG) + } + + companion object { + private const val TAG = "FileDisplayDialogs" + private const val TERMS_OF_SERVICE_TAG = "DIALOG_TAG_SHOW_TOS" + private const val NO_NEUTRAL_BUTTON = -1 + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/filedisplayactivity/FileDisplayUploadHandler.kt b/app/src/main/java/com/owncloud/android/ui/activity/filedisplayactivity/FileDisplayUploadHandler.kt new file mode 100644 index 000000000000..2eeff506e423 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/filedisplayactivity/FileDisplayUploadHandler.kt @@ -0,0 +1,192 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.activity.filedisplayactivity + +import android.content.Intent +import android.net.Uri +import android.os.Parcelable +import androidx.core.util.Function +import androidx.lifecycle.lifecycleScope +import com.nextcloud.client.jobs.upload.FileUploadHelper +import com.nextcloud.client.jobs.upload.FileUploadWorker +import com.nextcloud.client.network.ConnectivityService +import com.nextcloud.model.OCUploadLocalPathData +import com.nextcloud.utils.SnackbarUtil +import com.nextcloud.utils.fileNameValidator.FileNameValidator.checkFolderPath +import com.owncloud.android.R +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.ui.activity.FileDisplayActivity +import com.owncloud.android.ui.activity.UploadFilesActivity +import com.owncloud.android.ui.asynctasks.CheckAvailableSpaceTask +import com.owncloud.android.ui.asynctasks.CheckAvailableSpaceTask.CheckAvailableSpaceListener +import com.owncloud.android.ui.helpers.FileOperationsHelper +import com.owncloud.android.ui.helpers.UriUploader +import com.owncloud.android.utils.StringUtils +import com.owncloud.android.utils.UriUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.apache.commons.io.FilenameUtils +import java.io.File + +class FileDisplayUploadHandler( + private val activity: FileDisplayActivity, + private val connectivityService: ConnectivityService, + private val storageManager: FileDataStorageManager +) { + + // region Content from other apps + fun uploadContentFromApps(contentIntent: Intent, resultCode: Int, autoRename: Boolean) { + val user = activity.user.orElse(null) ?: return + + val clipData = contentIntent.clipData + val urisToUpload: List = if (clipData != null && clipData.itemCount > 0) { + List(clipData.itemCount) { clipData.getItemAt(it)?.uri } + } else { + listOf(contentIntent.data) + } + + val behaviour = if (resultCode == UploadFilesActivity.RESULT_OK_AND_MOVE) { + FileUploadWorker.LOCAL_BEHAVIOUR_MOVE + } else { + FileUploadWorker.LOCAL_BEHAVIOUR_COPY + } + + val remotePath = activity.getCurrentDir()?.remotePath ?: OCFile.ROOT_PATH + val displayNameTransformer = if (autoRename) Function { timestampedFileName(it) } else null + + UriUploader( + activity, + urisToUpload, + remotePath, + user, + behaviour, + showWaitingDialog = false, + copyTmpTaskListener = null, + fileDisplayNameTransformer = displayNameTransformer + ).uploadUris() + } + + private fun timestampedFileName(uri: Uri): String? { + val displayName = UriUtils.getDisplayNameForUri(uri, activity.applicationContext) + if (displayName.isNullOrEmpty()) return null + + return FileOperationsHelper.getTimestampedFileName("." + FilenameUtils.getExtension(displayName)) + } + // endregion + + // region Camera + fun uploadFromCamera(isVideo: Boolean) { + val cameraFilePath = FileOperationsHelper.createCameraFile(activity, isVideo).absolutePath + + val listener = object : CheckAvailableSpaceListener { + override fun onCheckAvailableSpaceStart() { + Log_OC.d(this, "onCheckAvailableSpaceStart") + } + + override fun onCheckAvailableSpaceFinish(hasEnoughSpaceAvailable: Boolean, vararg filesToUpload: String?) { + Log_OC.d(this, "onCheckAvailableSpaceFinish") + if (!hasEnoughSpaceAvailable) return + + val capturedFilePath = filesToUpload.firstOrNull() ?: return + renameAndUploadCapturedFile(File(capturedFilePath), isVideo) + } + } + + CheckAvailableSpaceTask(listener, cameraFilePath).execute() + } + + private fun renameAndUploadCapturedFile(capturedFile: File, isVideo: Boolean) { + val capturedFileName = if (isVideo) { + FileOperationsHelper.getCapturedVideoName() + } else { + FileOperationsHelper.getCapturedImageName() + } + val renamedFile = File(capturedFile.parent + OCFile.PATH_SEPARATOR + capturedFileName) + + if (!capturedFile.renameTo(renamedFile)) { + SnackbarUtil.show(activity, R.string.error_uploading_direct_camera_upload) + return + } + + val localBasePath = renamedFile.parentFile?.absolutePath ?: return showNoFileSelected() + uploadFiles(localBasePath, arrayOf(renamedFile.absolutePath), FileUploadWorker.LOCAL_BEHAVIOUR_DELETE) + } + // endregion + + // region Files from the file system + fun uploadFilesFromFileSystem(data: Intent, resultCode: Int) { + val filePaths = data.getStringArrayExtra(UploadFilesActivity.EXTRA_CHOSEN_FILES) ?: return + val localBasePath = data.getStringExtra(UploadFilesActivity.LOCAL_BASE_PATH) ?: return showNoFileSelected() + + val behaviour = when (resultCode) { + UploadFilesActivity.RESULT_OK_AND_MOVE -> FileUploadWorker.LOCAL_BEHAVIOUR_MOVE + UploadFilesActivity.RESULT_OK_AND_DELETE -> FileUploadWorker.LOCAL_BEHAVIOUR_DELETE + else -> FileUploadWorker.LOCAL_BEHAVIOUR_FORGET + } + uploadFiles(localBasePath, filePaths, behaviour) + } + + private fun showNoFileSelected() { + Log_OC.d(TAG, "User clicked on 'Update' with no selection") + SnackbarUtil.show(activity, R.string.filedisplay_no_file_selected) + } + + private fun uploadFiles(localBasePath: String, filePaths: Array, behaviour: Int) { + val normalizedLocalBasePath = if (localBasePath.endsWith("/")) localBasePath else "$localBasePath/" + val remotePathBase = activity.getCurrentDir()?.remotePath + val remotePaths = filePaths + .map { (remotePathBase ?: "") + StringUtils.removePrefix(it, normalizedLocalBasePath) } + .toTypedArray() + + connectivityService.isNetworkAndServerAvailable { isAvailable -> + if (isAvailable) { + uploadNewFiles(remotePathBase, filePaths, remotePaths, behaviour) + } else { + addCreateFileOfflineOperation(filePaths, remotePaths) + } + } + } + + private fun uploadNewFiles( + remotePathBase: String?, + filePaths: Array, + remotePaths: Array, + behaviour: Int + ) { + val capability = activity.capabilities.orElse(null) ?: return + + val isValidFolderPath = remotePathBase?.let { checkFolderPath(it, capability, activity) } + if (isValidFolderPath == false) { + SnackbarUtil.show( + activity, + R.string.file_name_validator_error_contains_reserved_names_or_invalid_characters + ) + return + } + + val user = activity.user.orElse(null) ?: return + val data = OCUploadLocalPathData.forFile(user, filePaths, remotePaths, behaviour, createRemoteFolder = true) + FileUploadHelper.instance().uploadNewFiles(data) + } + + private fun addCreateFileOfflineOperation(filePaths: Array, remotePaths: Array) { + activity.lifecycleScope.launch { + withContext(Dispatchers.IO) { + storageManager.addCreateFileOfflineOperation(filePaths, remotePaths) + } + activity.refreshCurrentDirectory() + } + } + // endregion + + companion object { + private const val TAG = "FileDisplayUploadHandler" + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/filedisplayactivity/FileDownloadIndicator.kt b/app/src/main/java/com/owncloud/android/ui/activity/filedisplayactivity/FileDownloadIndicator.kt new file mode 100644 index 000000000000..75617c00d561 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/filedisplayactivity/FileDownloadIndicator.kt @@ -0,0 +1,15 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.activity.filedisplayactivity + +import androidx.annotation.DrawableRes +import com.owncloud.android.R + +enum class FileDownloadIndicator(@param:DrawableRes val iconId: Int) { + Downloading(R.drawable.ic_synchronizing), + Downloaded(R.drawable.ic_synced) +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/filedisplayactivity/LocalIntentReceiver.kt b/app/src/main/java/com/owncloud/android/ui/activity/filedisplayactivity/LocalIntentReceiver.kt new file mode 100644 index 000000000000..d3242761e19f --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/filedisplayactivity/LocalIntentReceiver.kt @@ -0,0 +1,15 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.activity.filedisplayactivity + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent + +class LocalIntentReceiver(private val onIntentReceived: (Intent) -> Unit) : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent) = onIntentReceived(intent) +} diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/albums/AlbumOperationListener.kt b/app/src/main/java/com/owncloud/android/ui/fragment/albums/AlbumOperationListener.kt index 14a5d6626970..823b3c356fd9 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/albums/AlbumOperationListener.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/albums/AlbumOperationListener.kt @@ -110,8 +110,8 @@ class AlbumOperationListener(private val activity: FileDisplayActivity) { private fun showUntrustedCertDialog(result: RemoteOperationResult<*>) { if (result.isSslRecoverableException) { - activity.mLastSslUntrustedServerResult = result - activity.showUntrustedCertDialog(activity.mLastSslUntrustedServerResult) + activity.lastSslUntrustedServerResult = result + activity.showUntrustedCertDialog(activity.lastSslUntrustedServerResult) } }