diff --git a/app/src/main/java/com/nextcloud/ui/fileactions/FileActionHandler.kt b/app/src/main/java/com/nextcloud/ui/fileactions/FileActionHandler.kt index 115178418cc6..2eabfc2e1499 100644 --- a/app/src/main/java/com/nextcloud/ui/fileactions/FileActionHandler.kt +++ b/app/src/main/java/com/nextcloud/ui/fileactions/FileActionHandler.kt @@ -12,6 +12,7 @@ import com.nextcloud.client.editimage.EditImageActivity import com.nextcloud.client.jobs.BackgroundJobManager import com.nextcloud.utils.EditorUtils import com.nextcloud.utils.ShortcutUtil +import com.nextcloud.utils.SnackbarUtil import com.nextcloud.utils.extensions.handle import com.owncloud.android.R import com.owncloud.android.datamodel.OCFile @@ -45,7 +46,11 @@ class FileActionHandler @Inject constructor( @Suppress("CyclomaticComplexMethod") private fun file(@IdRes itemId: Int, file: OCFile, host: FileActionHost): Boolean { when (itemId) { - R.id.action_send_share_file -> host.containerActivity.showDetails(file, SHARING_TAB) + R.id.action_send_share_file -> sendShareFile(file, host) + + R.id.action_see_details -> host.containerActivity.showDetails(file) + + R.id.action_open_file_with -> openFile(file, host) R.id.action_rename_file -> RenameFileDialogFragment.newInstance(file, host.parentFolder) .show(host.fragmentManager, FileDetailFragment.FTAG_RENAME_FILE) @@ -75,7 +80,7 @@ class FileActionHandler @Inject constructor( .show(host.fragmentManager, ConfirmationDialogFragment.FTAG_CONFIRMATION) R.id.action_cancel_sync -> files.forEach { - host.fda?.cancelTransference(it) + host.fda?.cancelTransference(it) ?: host.helper?.cancelTransference(it) } R.id.action_favorite -> setFavorite(files, true, host) @@ -88,6 +93,20 @@ class FileActionHandler @Inject constructor( return true } + private fun sendShareFile(file: OCFile, host: FileActionHost) { + if (file.isSharedWithMe && !file.canReshare()) { + SnackbarUtil.show(host.view, R.string.resharing_is_not_allowed) + return + } + + host.helper?.sendShareFile(file) + } + + private fun openFile(file: OCFile, host: FileActionHost) { + host.helper?.openFile(file) + host.onFileOpened() + } + private fun setFavorite(files: Collection, isFavorite: Boolean, host: FileActionHost) { host.helper?.toggleFavoriteFiles(files, isFavorite) host.onFavoriteChanged(isFavorite) @@ -100,7 +119,7 @@ class FileActionHandler @Inject constructor( TextEditorWebView.startTextEditor(file, context) EditImageActivity.canBePreviewed(file) -> - host.fda?.startImageEditor(file) + host.fda?.startImageEditor(file) ?: host.previewMediaActivity?.startImageEditor(file) else -> host.helper?.openFileAsRichDocument(file, context) } @@ -141,7 +160,6 @@ class FileActionHandler @Inject constructor( } companion object { - private const val SHARING_TAB = 1 private const val NO_NEUTRAL_BUTTON = -1 } } diff --git a/app/src/main/java/com/nextcloud/ui/fileactions/FileActionHost.kt b/app/src/main/java/com/nextcloud/ui/fileactions/FileActionHost.kt index 3540e257cf2e..e795cf502841 100644 --- a/app/src/main/java/com/nextcloud/ui/fileactions/FileActionHost.kt +++ b/app/src/main/java/com/nextcloud/ui/fileactions/FileActionHost.kt @@ -15,6 +15,7 @@ import com.owncloud.android.ui.activity.FileActivity import com.owncloud.android.ui.activity.FileDisplayActivity import com.owncloud.android.ui.fragment.FileFragment import com.owncloud.android.ui.helpers.FileOperationsHelper +import com.owncloud.android.ui.preview.media.PreviewMediaActivity class FileActionHost @JvmOverloads constructor( val containerActivity: FileFragment.ContainerActivity, @@ -22,6 +23,7 @@ class FileActionHost @JvmOverloads constructor( val view: View?, val parentFolder: OCFile?, val actionMode: ActionMode? = null, + val onFileOpened: () -> Unit = {}, val onFavoriteChanged: (isFavorite: Boolean) -> Unit = {} ) { val helper: FileOperationsHelper? @@ -33,6 +35,9 @@ class FileActionHost @JvmOverloads constructor( val fda: FileDisplayActivity? get() = containerActivity as? FileDisplayActivity + val previewMediaActivity: PreviewMediaActivity? + get() = containerActivity as? PreviewMediaActivity + val context: Context? get() = view?.context } diff --git a/app/src/main/java/com/nextcloud/utils/extensions/FileOperationHelperExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/FileOperationHelperExtensions.kt index 8f5a06143cd1..11cb321b3169 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/FileOperationHelperExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/FileOperationHelperExtensions.kt @@ -17,7 +17,6 @@ import com.owncloud.android.ui.helpers.FileOperationsHelper fun FileOperationsHelper.handle(@IdRes itemId: Int, file: OCFile, view: View?): Boolean { when (itemId) { R.id.action_send_file -> sendShareFile(file, true) - R.id.action_open_file_with -> openFile(file) R.id.action_stream_media -> streamMediaFile(file) R.id.action_set_as_wallpaper -> setPictureAs(file, view) R.id.action_encrypted -> toggleEncryption(file, true) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt index 320cdb5e496d..01f945d5f4bb 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.kt @@ -944,6 +944,9 @@ open class OCFileListFragment : private fun handleListAction(@IdRes itemId: Int, checkedFiles: Set): Boolean { when { + itemId == R.id.action_send_share_file && checkedFiles.size == SINGLE_SELECTION -> + containerActivity?.showDetails(checkedFiles.first(), SHARING_TAB) + itemId == R.id.action_see_details && checkedFiles.size == SINGLE_SELECTION -> showDetailsAndHideSortGroup(checkedFiles.first()) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/albums/AlbumItemsFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/albums/AlbumItemsFragment.kt index 5fab841eb5e6..5e768d1a86be 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/albums/AlbumItemsFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/albums/AlbumItemsFragment.kt @@ -47,6 +47,8 @@ import com.nextcloud.client.utils.IntentUtil import com.nextcloud.client.utils.Throttler import com.nextcloud.repository.ClientRepository import com.nextcloud.ui.albumItemActions.AlbumItemActionsBottomSheet +import com.nextcloud.ui.fileactions.FileActionHandler +import com.nextcloud.ui.fileactions.FileActionHost import com.nextcloud.ui.fileactions.FileActionsBottomSheet import com.nextcloud.utils.SnackbarUtil import com.nextcloud.utils.extensions.getTypedActivity @@ -116,6 +118,9 @@ class AlbumItemsFragment : @Inject lateinit var viewThemeUtils: ViewThemeUtils + @Inject + lateinit var fileActionHandler: FileActionHandler + @Inject lateinit var accountManager: UserAccountManager @@ -622,24 +627,21 @@ class AlbumItemsFragment : ?.getClientIntegrationEndpoints(Type.CONTEXT_MENU, checkedFiles.first().mimeType) .orEmpty() - private fun onFileActionChosen(@IdRes itemId: Int, checkedFiles: Set): Boolean { - if (checkedFiles.isEmpty()) { - return false - } - - val fileOperationsHelper = containerActivity?.fileOperationsHelper + private fun onFileActionChosen(@IdRes itemId: Int, checkedFiles: Set) { + if (checkedFiles.isEmpty()) return when (itemId) { R.id.action_remove_file -> confirmFilesRemoval(checkedFiles) - R.id.action_favorite -> fileOperationsHelper?.toggleFavoriteFiles(checkedFiles, true) - R.id.action_unset_favorite -> fileOperationsHelper?.toggleFavoriteFiles(checkedFiles, false) - R.id.action_open_file_with -> fileOperationsHelper?.openFile(checkedFiles.first()) - R.id.action_stream_media -> fileOperationsHelper?.streamMediaFile(checkedFiles.first()) + R.id.action_select_all_action_menu -> selectAllFiles(true) + R.id.action_deselect_all_action_menu -> selectAllFiles(false) - } - return true + else -> containerActivity?.let { + val host = FileActionHost(it, parentFragmentManager, view, null) + fileActionHandler.handle(itemId, checkedFiles, host) + } + } } @Subscribe(threadMode = ThreadMode.BACKGROUND) diff --git a/app/src/main/java/com/owncloud/android/ui/preview/media/PreviewMediaFragment.kt b/app/src/main/java/com/owncloud/android/ui/preview/media/PreviewMediaFragment.kt index a5ada2b3d032..8306217a58e4 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/media/PreviewMediaFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/preview/media/PreviewMediaFragment.kt @@ -46,11 +46,12 @@ import com.github.chrisbanes.photoview.PhotoView import com.google.android.material.snackbar.Snackbar import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.di.Injectable -import com.nextcloud.client.jobs.BackgroundJobManager import com.nextcloud.client.network.ConnectivityService import com.nextcloud.client.player.model.file.PlaybackCollection import com.nextcloud.repository.ClientRepository import com.nextcloud.ui.fileactions.FileAction +import com.nextcloud.ui.fileactions.FileActionHandler +import com.nextcloud.ui.fileactions.FileActionHost import com.nextcloud.ui.fileactions.FileActionsBottomSheet.Companion.newInstance import com.nextcloud.utils.SnackbarUtil import com.nextcloud.utils.extensions.clickWithDebounce @@ -67,8 +68,6 @@ import com.owncloud.android.datamodel.ThumbnailsCacheManager import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.operations.FetchRemoteFileOperation import com.owncloud.android.ui.activity.FileActivity -import com.owncloud.android.ui.dialog.ConfirmationDialogFragment -import com.owncloud.android.ui.dialog.RemoveFilesDialogFragment import com.owncloud.android.ui.fragment.FileFragment import com.owncloud.android.utils.BitmapUtils import com.owncloud.android.utils.MimeTypeUtil @@ -119,10 +118,10 @@ class PreviewMediaFragment : lateinit var accountManager: UserAccountManager @Inject - lateinit var backgroundJobManager: BackgroundJobManager + lateinit var viewThemeUtils: ViewThemeUtils @Inject - lateinit var viewThemeUtils: ViewThemeUtils + lateinit var fileActionHandler: FileActionHandler private lateinit var binding: PreviewImageFragmentBinding @@ -416,54 +415,12 @@ class PreviewMediaFragment : .show(fragmentManager, "actions") } - /** - * {@inheritDoc} - */ - @Suppress("CyclomaticComplexMethod") private fun onFileActionChosen(itemId: Int) { - if (itemId == R.id.action_send_share_file) { - if (file.isSharedWithMe && !file.canReshare()) { - Snackbar.make(requireView(), R.string.resharing_is_not_allowed, Snackbar.LENGTH_LONG).show() - } else { - containerActivity.fileOperationsHelper.sendShareFile(file) - } - } else if (itemId == R.id.action_send_file) { - containerActivity.fileOperationsHelper.sendShareFile(file, true) - } else if (itemId == R.id.action_open_file_with) { - openFile() - } else if (itemId == R.id.action_remove_file) { - val dialog = RemoveFilesDialogFragment.newInstance(file) - dialog.show(parentFragmentManager, ConfirmationDialogFragment.FTAG_CONFIRMATION) - } else if (itemId == R.id.action_see_details) { - seeDetails() - } else if (itemId == R.id.action_download_file || itemId == R.id.action_sync_file) { - if (containerActivity is FileActivity) { - val activity = containerActivity as FileActivity - activity.showSyncLoadingDialog(file.isFolder) - } - containerActivity.fileOperationsHelper.syncFileOrFolder(file) - } else if (itemId == R.id.action_cancel_sync) { - containerActivity.fileOperationsHelper.cancelTransference(file) - } else if (itemId == R.id.action_set_as_wallpaper) { - containerActivity.fileOperationsHelper.setPictureAs(file, imageView) - } else if (itemId == R.id.action_export_file) { - val list = ArrayList() - list.add(file) - containerActivity.fileOperationsHelper.exportFiles( - list, - context, - view, - backgroundJobManager - ) - } else if (itemId == R.id.action_edit) { - (requireActivity() as PreviewMediaActivity).startImageEditor(file) - } else if (itemId == R.id.action_add_to_album) { - containerActivity.fileOperationsHelper.addFileToAlbum(listOf(file)) - } - } + val file = file ?: return + val containerActivity = containerActivity ?: return - private fun seeDetails() { - containerActivity.showDetails(file) + val host = FileActionHost(containerActivity, parentFragmentManager, view, null, onFileOpened = ::finish) + fileActionHandler.handle(itemId, listOf(file), host) } @SuppressFBWarnings("Dm") @@ -475,11 +432,6 @@ class PreviewMediaFragment : /** * Opens the previewed image with an external application. */ - private fun openFile() { - containerActivity.fileOperationsHelper.openFile(file) - finish() - } - @SuppressLint("StaticFieldLeak") private inner class LoadBitmapTask(imageView: PhotoView, infoView: LinearLayout, progressView: FrameLayout) : AsyncTask() { @@ -500,7 +452,7 @@ class PreviewMediaFragment : private var mErrorMessageId = 0 private val paramsLength = 1 - @Suppress("MagicNumber", "LongMethod", "NestedBlockDepth", "CyclomaticComplexMethod") + @Suppress("TooGenericExceptionCaught", "MagicNumber", "ReturnCount", "LongMethod", "NestedBlockDepth") @Deprecated("Deprecated in Java") override fun doInBackground(vararg params: OCFile?): LoadImage? { Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_MORE_FAVORABLE) diff --git a/app/src/main/java/com/owncloud/android/ui/preview/media/PreviewPlaybackFragment.kt b/app/src/main/java/com/owncloud/android/ui/preview/media/PreviewPlaybackFragment.kt index 1f38e0d939d5..2c30fb8b305b 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/media/PreviewPlaybackFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/preview/media/PreviewPlaybackFragment.kt @@ -23,9 +23,7 @@ import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope -import com.google.android.material.snackbar.Snackbar import com.nextcloud.client.account.UserAccountManager -import com.nextcloud.client.jobs.BackgroundJobManager import com.nextcloud.client.player.media3.PlaybackModel import com.nextcloud.client.player.media3.PlaybackQueueLoader import com.nextcloud.client.player.model.PlayerThumbnailLoader @@ -40,6 +38,8 @@ import com.nextcloud.client.player.util.PlayerUtil.isPictureInPictureAllowed import com.nextcloud.client.player.util.PlayerUtil.ownsPlayback import com.nextcloud.client.player.util.PlayerUtil.toPlaybackFile import com.nextcloud.ui.fileactions.FileAction +import com.nextcloud.ui.fileactions.FileActionHandler +import com.nextcloud.ui.fileactions.FileActionHost import com.nextcloud.ui.fileactions.FileActionsBottomSheet import com.nextcloud.utils.SnackbarUtil import com.nextcloud.utils.extensions.addPaddingForNavBar @@ -51,8 +51,6 @@ import com.owncloud.android.databinding.PreviewPlaybackFragmentBinding import com.owncloud.android.datamodel.OCFile import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.operations.FetchRemoteFileOperation -import com.owncloud.android.ui.dialog.ConfirmationDialogFragment -import com.owncloud.android.ui.dialog.RemoveFilesDialogFragment import com.owncloud.android.utils.MimeTypeUtil import com.owncloud.android.utils.theme.ViewThemeUtils import dagger.android.support.AndroidSupportInjection @@ -96,10 +94,10 @@ class PreviewPlaybackFragment : lateinit var accountManager: UserAccountManager @Inject - lateinit var backgroundJobManager: BackgroundJobManager + lateinit var viewThemeUtils: ViewThemeUtils @Inject - lateinit var viewThemeUtils: ViewThemeUtils + lateinit var fileActionHandler: FileActionHandler private lateinit var binding: PreviewPlaybackFragmentBinding private lateinit var file: OCFile @@ -220,50 +218,20 @@ class PreviewPlaybackFragment : } } - @Suppress("CyclomaticComplexMethod") private fun onFileActionChosen(itemId: Int) { val previewActivity = previewActivity ?: return - val fileOperationsHelper = previewActivity.fileOperationsHelper when (itemId) { - R.id.action_see_details -> previewActivity.showDetails(file) - R.id.action_download_file -> previewActivity.requestForDownload(file) - R.id.action_export_file -> fileOperationsHelper.exportFiles( - arrayListOf(file), - context, - view, - backgroundJobManager - ) - - R.id.action_send_share_file -> if (file.isSharedWithMe && !file.canReshare()) { - Snackbar.make(requireView(), R.string.resharing_is_not_allowed, Snackbar.LENGTH_LONG).show() - } else { - fileOperationsHelper.sendShareFile(file) - } - - R.id.action_send_file -> fileOperationsHelper.sendShareFile(file, true) - - R.id.action_open_file_with -> fileOperationsHelper.openFile(file) - - R.id.action_stream_media -> { - playbackModel.pause() - fileOperationsHelper.streamMediaFile(file) - } + else -> { + if (itemId == R.id.action_stream_media || itemId == R.id.action_remove_file) { + playbackModel.pause() + } - R.id.action_remove_file -> { - playbackModel.pause() - RemoveFilesDialogFragment.newInstance( - file - ).show(parentFragmentManager, ConfirmationDialogFragment.FTAG_CONFIRMATION) + val host = FileActionHost(previewActivity, parentFragmentManager, view, null) + fileActionHandler.handle(itemId, listOf(file), host) } - - R.id.action_add_to_album -> fileOperationsHelper.addFileToAlbum(listOf(file)) - - R.id.action_lock_file -> fileOperationsHelper.toggleFileLock(file, true) - - R.id.action_unlock_file -> fileOperationsHelper.toggleFileLock(file, false) } } diff --git a/app/src/main/java/com/owncloud/android/ui/preview/text/PreviewTextFileFragment.java b/app/src/main/java/com/owncloud/android/ui/preview/text/PreviewTextFileFragment.java deleted file mode 100644 index 845fede2a346..000000000000 --- a/app/src/main/java/com/owncloud/android/ui/preview/text/PreviewTextFileFragment.java +++ /dev/null @@ -1,372 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2019 Tobias Kaminsky - * SPDX-FileCopyrightText: 2019 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.owncloud.android.ui.preview.text; - -import android.annotation.SuppressLint; -import android.os.AsyncTask; -import android.os.Bundle; -import android.os.Handler; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.widget.FrameLayout; -import android.widget.TextView; - -import com.nextcloud.client.account.User; -import com.nextcloud.ui.fileactions.FileAction; -import com.nextcloud.ui.fileactions.FileActionsBottomSheet; -import com.nextcloud.utils.SnackbarUtil; -import com.nextcloud.utils.extensions.BundleExtensionsKt; -import com.nextcloud.utils.extensions.FileExtensionsKt; -import com.owncloud.android.R; -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.ui.activity.FileActivity; -import com.owncloud.android.ui.activity.TextEditorWebView; -import com.owncloud.android.ui.dialog.ConfirmationDialogFragment; -import com.owncloud.android.ui.dialog.RemoveFilesDialogFragment; -import com.owncloud.android.utils.MimeTypeUtil; - -import org.mozilla.universalchardet.ReaderFactory; - -import java.io.BufferedWriter; -import java.io.File; -import java.io.IOException; -import java.io.Reader; -import java.io.StringWriter; -import java.lang.ref.WeakReference; -import java.util.LinkedList; -import java.util.List; -import java.util.Scanner; - -import androidx.annotation.NonNull; -import androidx.appcompat.widget.SearchView; -import androidx.core.view.MenuItemCompat; -import androidx.fragment.app.FragmentManager; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - -public class PreviewTextFileFragment extends PreviewTextFragment { - private static final String EXTRA_FILE = "FILE"; - private static final String EXTRA_USER = "USER"; - private static final String EXTRA_OPEN_SEARCH = "SEARCH"; - private static final String EXTRA_SEARCH_QUERY = "SEARCH_QUERY"; - - private static final String TAG = PreviewTextFileFragment.class.getSimpleName(); - - private TextLoadAsyncTask textLoadAsyncTask; - private User user; - - public static PreviewTextFileFragment create(User user, OCFile file, boolean openSearch, String searchQuery) { - Bundle args = new Bundle(); - args.putParcelable(EXTRA_FILE, file); - args.putParcelable(EXTRA_USER, user); - args.putBoolean(EXTRA_OPEN_SEARCH, openSearch); - args.putString(EXTRA_SEARCH_QUERY, searchQuery); - PreviewTextFileFragment fragment = new PreviewTextFileFragment(); - fragment.setArguments(args); - return fragment; - } - - /** - * Creates an empty fragment for previews. - *

- * MUST BE KEPT: the system uses it when tries to re-instantiate a fragment automatically (for instance, when the - * device is turned a aside). - *

- * DO NOT CALL IT: an {@link OCFile} and {@link User} must be provided for a successful construction - */ - public PreviewTextFileFragment() { - super(); - user = null; - } - - /** - * {@inheritDoc} - */ - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setHasOptionsMenu(true); - - OCFile file = getFile(); - - Bundle args = getArguments(); - - if (file == null) { - file = BundleExtensionsKt.getParcelableArgument(args, EXTRA_FILE, OCFile.class); - } - - if (user == null) { - user = BundleExtensionsKt.getParcelableArgument(args, EXTRA_USER, User.class); - } - - if (args.containsKey(EXTRA_SEARCH_QUERY)) { - searchQuery = args.getString(EXTRA_SEARCH_QUERY); - } - searchOpen = args.getBoolean(EXTRA_OPEN_SEARCH, false); - - if (savedInstanceState == null) { - if (file == null) { - throw new IllegalStateException("Instanced with a NULL OCFile"); - } - if (user == null) { - throw new IllegalStateException("Instanced with a NULL ownCloud Account"); - } - } else { - file = BundleExtensionsKt.getParcelableArgument(savedInstanceState, EXTRA_FILE, OCFile.class); - user = BundleExtensionsKt.getParcelableArgument(savedInstanceState, EXTRA_USER, User.class); - } - - handler = new Handler(); - setFile(file); - } - - /** - * {@inheritDoc} - */ - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - FileExtensionsKt.logFileSize(getFile(), TAG); - outState.putParcelable(PreviewTextFileFragment.EXTRA_FILE, getFile()); - outState.putParcelable(PreviewTextFileFragment.EXTRA_USER, user); - super.onSaveInstanceState(outState); - } - - @Override - public void loadAndShowTextPreview() { - textLoadAsyncTask = new TextLoadAsyncTask(new WeakReference<>(binding.textPreview), - new WeakReference<>(binding.emptyListProgress)); - textLoadAsyncTask.execute(getFile().getStoragePath()); - } - - /** - * Reads the file to preview and shows its contents. Too critical to be anonymous. - */ - @SuppressLint("StaticFieldLeak") - private class TextLoadAsyncTask extends AsyncTask { - private static final int PARAMS_LENGTH = 1; - private final WeakReference textViewReference; - private final WeakReference progressViewReference; - - private TextLoadAsyncTask(WeakReference textView, WeakReference progressView) { - textViewReference = textView; - progressViewReference = progressView; - } - - @Override - protected void onPreExecute() { - // not used at the moment - } - - @Override - protected StringWriter doInBackground(Object... params) { - if (params.length != PARAMS_LENGTH) { - throw new IllegalArgumentException("The parameter to " + TextLoadAsyncTask.class.getName() - + " must be (1) the file location"); - } - String location = (String) params[0]; - - Scanner scanner = null; - StringWriter source = new StringWriter(); - BufferedWriter bufferedWriter = new BufferedWriter(source); - Reader reader = null; - - try { - File file = new File(location); - reader = ReaderFactory.createReaderFromFile(file); - scanner = new Scanner(reader); - - while (scanner.hasNextLine()) { - bufferedWriter.append(scanner.nextLine()); - if (scanner.hasNextLine()) { - bufferedWriter.append("\n"); - } - } - bufferedWriter.close(); - IOException exc = scanner.ioException(); - if (exc != null) { - throw exc; - } - } catch (IOException e) { - Log_OC.e(TAG, e.getMessage(), e); - finish(); - } finally { - if (reader != null) { - try { - reader.close(); - } catch (IOException e) { - Log_OC.e(TAG, e.getMessage(), e); - finish(); - } - } - if (scanner != null) { - scanner.close(); - } - } - return source; - } - - @Override - @SuppressFBWarnings("STT") - protected void onPostExecute(final StringWriter stringWriter) { - final TextView textView = textViewReference.get(); - - if (textView != null) { - originalText = stringWriter.toString(); - setText(textView, originalText, getFile(), requireActivity(), false, false, viewThemeUtils); - - if (searchView != null) { - searchView.setOnQueryTextListener(PreviewTextFileFragment.this); - - if (searchOpen && searchView != null) { - searchView.setQuery(searchQuery, true); - } - } - - textView.setVisibility(View.VISIBLE); - } - - final FrameLayout progress = progressViewReference.get(); - if (progress != null) { - progress.setVisibility(View.GONE); - } - } - } - - /** - * {@inheritDoc} - */ - @Override - public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); - inflater.inflate(R.menu.custom_menu_placeholder, menu); - - MenuItem menuItem = menu.findItem(R.id.action_search); - menuItem.setVisible(true); - searchView = (SearchView) MenuItemCompat.getActionView(menuItem); - searchView.setMaxWidth(Integer.MAX_VALUE); - viewThemeUtils.androidx.themeToolbarSearchView(searchView); - - if (searchOpen) { - searchView.setIconified(false); - searchView.setQuery(searchQuery, false); - searchView.clearFocus(); - } - } - - @Override - public boolean onOptionsItemSelected(@NonNull MenuItem item) { - if (item.getItemId() == R.id.custom_menu_placeholder_item) { - final OCFile file = getFile(); - if (containerActivity.getStorageManager() != null && file != null) { - // Update the file - final OCFile updatedFile = containerActivity.getStorageManager().getFileById(file.getFileId()); - setFile(updatedFile); - - final OCFile fileNew = getFile(); - if (fileNew != null) { - showFileActions(file); - } - } - return true; - } - return super.onOptionsItemSelected(item); - } - - private void showFileActions(OCFile file) { - final var additionalFilter = FileAction.Companion.getFilePreviewActions(getFile()); - final FragmentManager fragmentManager = getChildFragmentManager(); - FileActionsBottomSheet.newInstance(file, false, additionalFilter) - .setResultListener(fragmentManager, this, this::onFileActionChosen) - .show(fragmentManager, "actions"); - } - - private void onFileActionChosen(final int itemId) { - if (itemId == R.id.action_send_share_file) { - if (getFile().isSharedWithMe() && !getFile().canReshare()) { - SnackbarUtil.show(this, R.string.resharing_is_not_allowed); - } else { - containerActivity.getFileOperationsHelper().sendShareFile(getFile()); - } - } else if (itemId == R.id.action_send_file) { - containerActivity.getFileOperationsHelper().sendShareFile(getFile(), true); - } else if (itemId == R.id.action_open_file_with) { - openFile(); - } else if (itemId == R.id.action_remove_file) { - RemoveFilesDialogFragment dialog = RemoveFilesDialogFragment.newInstance(getFile()); - dialog.show(getFragmentManager(), ConfirmationDialogFragment.FTAG_CONFIRMATION); - } else if (itemId == R.id.action_see_details) { - seeDetails(); - } else if (itemId == R.id.action_sync_file) { - if (containerActivity instanceof FileActivity activity) { - activity.showSyncLoadingDialog(getFile().isFolder()); - } - containerActivity.getFileOperationsHelper().syncFileOrFolder(getFile()); - } else if(itemId == R.id.action_cancel_sync){ - containerActivity.getFileOperationsHelper().cancelTransference(getFile()); - } else if (itemId == R.id.action_edit) { - TextEditorWebView.Companion.startTextEditor(getFile(), getContext()); - } - } - - /** - * Update the file of the fragment with file value - * - * @param file The new file to set - */ - public void updateFile(OCFile file) { - setFile(file); - } - - private void seeDetails() { - containerActivity.showDetails(getFile()); - } - - /** - * Opens the previewed file with an external application. - */ - private void openFile() { - containerActivity.getFileOperationsHelper().openFile(getFile()); - finish(); - } - - /** - * Helper method to test if an {@link OCFile} can be passed to a {@link PreviewTextFileFragment} to be previewed. - * - * @param file File to test if can be previewed. - * @return 'True' if the file can be handled by the fragment. - */ - public static boolean canBePreviewed(OCFile file) { - final List unsupportedTypes = new LinkedList<>(); - unsupportedTypes.add("text/richtext"); - unsupportedTypes.add("text/rtf"); - unsupportedTypes.add("text/calendar"); - unsupportedTypes.add("text/vnd.abc"); - unsupportedTypes.add("text/vnd.fmi.flexstor"); - unsupportedTypes.add("text/vnd.rn-realtext"); - unsupportedTypes.add("text/vnd.wap.wml"); - unsupportedTypes.add("text/vnd.wap.wmlscript"); - unsupportedTypes.add("text/html"); - return file != null && file.isDown() && MimeTypeUtil.isText(file) && - !unsupportedTypes.contains(file.getMimeType()) && - !unsupportedTypes.contains(MimeTypeUtil.getMimeTypeFromPath(file.getRemotePath())); - } - - - @Override - public void onStop() { - super.onStop(); - Log_OC.e(TAG, "onStop"); - - if (textLoadAsyncTask != null) { - textLoadAsyncTask.cancel(true); - } - } - -} diff --git a/app/src/main/java/com/owncloud/android/ui/preview/text/PreviewTextFileFragment.kt b/app/src/main/java/com/owncloud/android/ui/preview/text/PreviewTextFileFragment.kt new file mode 100644 index 000000000000..a1937f430f81 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/preview/text/PreviewTextFileFragment.kt @@ -0,0 +1,246 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.preview.text + +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import androidx.annotation.IdRes +import androidx.appcompat.widget.SearchView +import androidx.core.view.MenuProvider +import androidx.core.view.isGone +import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope +import com.nextcloud.client.account.User +import com.nextcloud.ui.fileactions.FileAction +import com.nextcloud.ui.fileactions.FileActionHandler +import com.nextcloud.ui.fileactions.FileActionHost +import com.nextcloud.ui.fileactions.FileActionsBottomSheet +import com.nextcloud.utils.extensions.getParcelableArgument +import com.owncloud.android.R +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.utils.MimeTypeUtil +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.mozilla.universalchardet.ReaderFactory +import java.io.File +import java.io.IOException +import java.io.Reader +import java.util.Scanner +import javax.inject.Inject + +class PreviewTextFileFragment : PreviewTextFragment() { + + @Inject + lateinit var fileActionHandler: FileActionHandler + + private var textLoadJob: Job? = null + private var user: User? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val args = requireArguments() + readSearchArguments(args) + file = if (savedInstanceState == null) requireFileAndUser(args) else restoreFileAndUser(savedInstanceState) + handler = Handler(Looper.getMainLooper()) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + addMenuProvider() + } + + override fun onSaveInstanceState(outState: Bundle) { + outState.putParcelable(EXTRA_FILE, file) + outState.putParcelable(EXTRA_USER, user) + super.onSaveInstanceState(outState) + } + + override fun onStop() { + super.onStop() + Log_OC.e(TAG, "onStop") + textLoadJob?.cancel() + } + + private fun readSearchArguments(args: Bundle) { + args.getString(EXTRA_SEARCH_QUERY)?.let { searchQuery = it } + searchOpen = args.getBoolean(EXTRA_OPEN_SEARCH, false) + } + + private fun requireFileAndUser(args: Bundle): OCFile { + val previewFile = checkNotNull(file ?: args.getParcelableArgument(EXTRA_FILE, OCFile::class.java)) { + "Instanced with a NULL OCFile" + } + user = checkNotNull(user ?: args.getParcelableArgument(EXTRA_USER, User::class.java)) { + "Instanced with a NULL ownCloud Account" + } + return previewFile + } + + private fun restoreFileAndUser(savedInstanceState: Bundle): OCFile? { + user = savedInstanceState.getParcelableArgument(EXTRA_USER, User::class.java) + return savedInstanceState.getParcelableArgument(EXTRA_FILE, OCFile::class.java) + } + + override fun loadAndShowTextPreview() { + val storagePath = file?.storagePath ?: return + textLoadJob = viewLifecycleOwner.lifecycleScope.launch { + val text = withContext(Dispatchers.IO) { readText(storagePath) } + showText(text) + } + } + + private fun readText(storagePath: String): String { + val text = StringBuilder() + try { + ReaderFactory.createBufferedReader(File(storagePath)).use { text.appendLines(it) } + } catch (e: IOException) { + Log_OC.e(TAG, e.message, e) + finish() + } + return text.toString() + } + + private fun StringBuilder.appendLines(reader: Reader) { + val scanner = Scanner(reader) + while (scanner.hasNextLine()) { + append(scanner.nextLine()) + if (scanner.hasNextLine()) { + append(LINE_SEPARATOR) + } + } + scanner.ioException()?.let { throw it } + } + + private fun showText(text: String) { + originalText = text + setText(binding.textPreview, text, file, requireActivity(), false, false, viewThemeUtils) + + searchView?.let { + it.setOnQueryTextListener(this) + if (searchOpen) { + it.setQuery(searchQuery, true) + } + } + + binding.textPreview.isVisible = true + binding.emptyListProgress.isGone = true + } + + private fun addMenuProvider() { + requireActivity().addMenuProvider( + object : MenuProvider { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.custom_menu_placeholder, menu) + setupSearchView(menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + if (menuItem.itemId != R.id.custom_menu_placeholder_item) { + return false + } + + refreshFileAndShowActions() + return true + } + }, + viewLifecycleOwner + ) + } + + private fun setupSearchView(menu: Menu) { + val searchMenuItem = menu.findItem(R.id.action_search) ?: return + searchMenuItem.isVisible = true + searchView = searchMenuItem.actionView as? SearchView + + val searchView = searchView ?: return + searchView.maxWidth = Int.MAX_VALUE + viewThemeUtils.androidx.themeToolbarSearchView(searchView) + + if (searchOpen) { + searchView.isIconified = false + searchView.setQuery(searchQuery, false) + searchView.clearFocus() + } + } + + private fun refreshFileAndShowActions() { + val currentFile = file ?: return + val storageManager = containerActivity?.storageManager ?: return + + file = storageManager.getFileById(currentFile.fileId) + if (file != null) { + showFileActions(currentFile) + } + } + + private fun showFileActions(actionsFile: OCFile) { + val additionalFilter = FileAction.getFilePreviewActions(file) + FileActionsBottomSheet.newInstance(actionsFile, false, additionalFilter) + .setResultListener(childFragmentManager, this) { itemId -> onFileActionChosen(itemId) } + .show(childFragmentManager, TAG_FILE_ACTIONS) + } + + private fun onFileActionChosen(@IdRes itemId: Int) { + val previewFile = file ?: return + val containerActivity = containerActivity ?: return + val host = FileActionHost(containerActivity, parentFragmentManager, view, null, onFileOpened = ::finish) + fileActionHandler.handle(itemId, listOf(previewFile), host) + } + + fun updateFile(file: OCFile?) { + this.file = file + } + + companion object { + private const val EXTRA_FILE = "FILE" + private const val EXTRA_USER = "USER" + private const val EXTRA_OPEN_SEARCH = "SEARCH" + private const val EXTRA_SEARCH_QUERY = "SEARCH_QUERY" + private const val TAG_FILE_ACTIONS = "actions" + private const val LINE_SEPARATOR = "\n" + + private val TAG: String = PreviewTextFileFragment::class.java.simpleName + + private val UNSUPPORTED_MIME_TYPES = setOf( + "text/richtext", + "text/rtf", + "text/calendar", + "text/vnd.abc", + "text/vnd.fmi.flexstor", + "text/vnd.rn-realtext", + "text/vnd.wap.wml", + "text/vnd.wap.wmlscript", + "text/html" + ) + + fun create(user: User, file: OCFile?, openSearch: Boolean, searchQuery: String?): PreviewTextFileFragment = + PreviewTextFileFragment().apply { + arguments = Bundle().apply { + putParcelable(EXTRA_FILE, file) + putParcelable(EXTRA_USER, user) + putBoolean(EXTRA_OPEN_SEARCH, openSearch) + putString(EXTRA_SEARCH_QUERY, searchQuery) + } + } + + fun canBePreviewed(file: OCFile?): Boolean = file != null && + file.isDown && + MimeTypeUtil.isText(file) && + file.mimeType !in UNSUPPORTED_MIME_TYPES && + MimeTypeUtil.getMimeTypeFromPath(file.remotePath) !in UNSUPPORTED_MIME_TYPES + } +}