Browse Source

Merge pull request #11008 from nextcloud/feature/file-actions-bottom-sheet

Redesign file actions menu as a BottomSheet
Álvaro Brey 2 years ago
parent
commit
75774ed2ce
47 changed files with 1536 additions and 1061 deletions
  1. BIN
      app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testFileActionsBottomSheet.png
  2. 38 52
      app/src/androidTest/java/com/owncloud/android/files/FileMenuFilterIT.kt
  3. 13 0
      app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.java
  4. 3 1
      app/src/debug/java/com/nextcloud/client/TestActivity.kt
  5. 0 78
      app/src/main/java/com/nextcloud/android/files/FileLockingMenuCustomization.kt
  6. 0 42
      app/src/main/java/com/nextcloud/android/files/ThemedPopupMenu.kt
  7. 4 0
      app/src/main/java/com/nextcloud/client/di/ComponentsModule.java
  8. 6 0
      app/src/main/java/com/nextcloud/client/di/ViewModelModule.kt
  9. 101 0
      app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt
  10. 338 0
      app/src/main/java/com/nextcloud/ui/fileactions/FileActionsBottomSheet.kt
  11. 167 0
      app/src/main/java/com/nextcloud/ui/fileactions/FileActionsViewModel.kt
  12. 47 0
      app/src/main/java/com/nextcloud/utils/EditorUtils.kt
  13. 55 0
      app/src/main/java/com/nextcloud/utils/MenuUtils.kt
  14. 128 267
      app/src/main/java/com/owncloud/android/files/FileMenuFilter.java
  15. 5 1
      app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java
  16. 4 4
      app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java
  17. 6 3
      app/src/main/java/com/owncloud/android/ui/activity/TextEditorWebView.kt
  18. 3 3
      app/src/main/java/com/owncloud/android/ui/adapter/OCFileListItemViewHolder.kt
  19. 5 4
      app/src/main/java/com/owncloud/android/ui/asynctasks/TextEditorLoadUrlTask.java
  20. 32 47
      app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java
  21. 7 5
      app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.java
  22. 6 1
      app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialogFragment.kt
  23. 34 46
      app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java
  24. 5 5
      app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java
  25. 43 66
      app/src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.java
  26. 39 76
      app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.java
  27. 39 46
      app/src/main/java/com/owncloud/android/ui/preview/PreviewTextFileFragment.java
  28. 2 2
      app/src/main/java/com/owncloud/android/ui/preview/pdf/PreviewPdfFragment.kt
  29. 8 0
      app/src/main/res/drawable/file_multiple.xml
  30. 8 0
      app/src/main/res/drawable/ic_decrypt.xml
  31. 8 0
      app/src/main/res/drawable/ic_encrypt.xml
  32. 8 0
      app/src/main/res/drawable/ic_export.xml
  33. 8 0
      app/src/main/res/drawable/ic_lock.xml
  34. 10 0
      app/src/main/res/drawable/ic_move.xml
  35. 10 0
      app/src/main/res/drawable/ic_rename.xml
  36. 8 0
      app/src/main/res/drawable/ic_wallpaper.xml
  37. 100 0
      app/src/main/res/layout/file_actions_bottom_sheet.xml
  38. 80 0
      app/src/main/res/layout/file_actions_bottom_sheet_item.xml
  39. 64 0
      app/src/main/res/layout/file_thumbnail.xml
  40. 12 40
      app/src/main/res/layout/list_item.xml
  41. 30 0
      app/src/main/res/menu/custom_menu_placeholder.xml
  42. 0 82
      app/src/main/res/menu/fragment_file_detail.xml
  43. 0 190
      app/src/main/res/menu/item_file.xml
  44. 1 0
      app/src/main/res/values/dims.xml
  45. 49 0
      app/src/main/res/values/ids.xml
  46. 1 0
      app/src/main/res/values/strings.xml
  47. 1 0
      app/src/main/res/values/styles.xml

BIN
app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testFileActionsBottomSheet.png


+ 38 - 52
app/src/androidTest/java/com/owncloud/android/files/FileMenuFilterIT.kt

@@ -20,14 +20,14 @@
  */
 package com.owncloud.android.files
 
-import android.view.Menu
-import androidx.appcompat.view.menu.MenuBuilder
 import androidx.test.core.app.launchActivity
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.nextcloud.client.TestActivity
 import com.nextcloud.client.account.User
+import com.nextcloud.utils.EditorUtils
 import com.owncloud.android.AbstractIT
 import com.owncloud.android.R
+import com.owncloud.android.datamodel.ArbitraryDataProvider
 import com.owncloud.android.datamodel.FileDataStorageManager
 import com.owncloud.android.datamodel.OCFile
 import com.owncloud.android.files.services.FileDownloader
@@ -67,6 +67,11 @@ class FileMenuFilterIT : AbstractIT() {
     @MockK
     private lateinit var mockOperationsServiceBinder: OperationsService.OperationsServiceBinder
 
+    @MockK
+    private lateinit var mockArbitraryDataProvider: ArbitraryDataProvider
+
+    private lateinit var editorUtils: EditorUtils
+
     @Before
     fun setup() {
         MockKAnnotations.init(this)
@@ -78,6 +83,8 @@ class FileMenuFilterIT : AbstractIT() {
         every { mockComponentsGetter.operationsServiceBinder } returns mockOperationsServiceBinder
         every { mockStorageManager.getFileById(any()) } returns OCFile("/")
         every { mockStorageManager.getFolderContent(any(), any()) } returns ArrayList<OCFile>()
+        every { mockArbitraryDataProvider.getValue(any<User>(), any()) } returns ""
+        editorUtils = EditorUtils(mockArbitraryDataProvider)
     }
 
     @Test
@@ -91,7 +98,7 @@ class FileMenuFilterIT : AbstractIT() {
         testLockingVisibilities(
             capability,
             file,
-            ExpectedLockVisibilities(lockFile = false, unlockFile = false, lockedBy = false, lockedUntil = false)
+            ExpectedLockVisibilities(lockFile = false, unlockFile = false)
         )
     }
 
@@ -107,7 +114,7 @@ class FileMenuFilterIT : AbstractIT() {
         testLockingVisibilities(
             capability,
             file,
-            ExpectedLockVisibilities(lockFile = true, unlockFile = false, lockedBy = false, lockedUntil = false)
+            ExpectedLockVisibilities(lockFile = true, unlockFile = false)
         )
     }
 
@@ -130,7 +137,7 @@ class FileMenuFilterIT : AbstractIT() {
         testLockingVisibilities(
             capability,
             file,
-            ExpectedLockVisibilities(lockFile = false, unlockFile = true, lockedBy = true, lockedUntil = true)
+            ExpectedLockVisibilities(lockFile = false, unlockFile = true)
         )
     }
 
@@ -152,7 +159,7 @@ class FileMenuFilterIT : AbstractIT() {
         testLockingVisibilities(
             capability,
             file,
-            ExpectedLockVisibilities(lockFile = false, unlockFile = false, lockedBy = true, lockedUntil = true)
+            ExpectedLockVisibilities(lockFile = false, unlockFile = false)
         )
     }
 
@@ -186,57 +193,48 @@ class FileMenuFilterIT : AbstractIT() {
 
         launchActivity<TestActivity>().use {
             it.onActivity { activity ->
-                val menu = getMenu(activity)
+                val filterFactory =
+                    FileMenuFilter.Factory(mockStorageManager, activity, editorUtils)
 
-                var sut = FileMenuFilter(encryptedFolder, mockComponentsGetter, activity, true, user)
-                sut.filter(menu, false)
+                var sut = filterFactory.newInstance(encryptedFolder, mockComponentsGetter, true, user)
+                var toHide = sut.getToHide(false)
 
                 // encrypted folder, with content
-                assertFalse(menu.findItem(R.id.action_unset_encrypted).isVisible)
-                assertFalse(menu.findItem(R.id.action_encrypted).isVisible)
+                assertTrue(toHide.contains(R.id.action_unset_encrypted))
+                assertTrue(toHide.contains(R.id.action_encrypted))
 
                 // encrypted, but empty folder
-                sut = FileMenuFilter(encryptedEmptyFolder, mockComponentsGetter, activity, true, user)
-                sut.filter(menu, false)
+                sut = filterFactory.newInstance(encryptedEmptyFolder, mockComponentsGetter, true, user)
+                toHide = sut.getToHide(false)
 
-                assertTrue(menu.findItem(R.id.action_unset_encrypted).isVisible)
-                assertFalse(menu.findItem(R.id.action_encrypted).isVisible)
+                assertFalse(toHide.contains(R.id.action_unset_encrypted))
+                assertTrue(toHide.contains(R.id.action_encrypted))
 
                 // regular folder, with content
-                sut = FileMenuFilter(normalFolder, mockComponentsGetter, activity, true, user)
-                sut.filter(menu, false)
+                sut = filterFactory.newInstance(normalFolder, mockComponentsGetter, true, user)
+                toHide = sut.getToHide(false)
 
-                assertFalse(menu.findItem(R.id.action_unset_encrypted).isVisible)
-                assertFalse(menu.findItem(R.id.action_encrypted).isVisible)
+                assertTrue(toHide.contains(R.id.action_unset_encrypted))
+                assertTrue(toHide.contains(R.id.action_encrypted))
 
                 // regular folder, without content
-                sut = FileMenuFilter(normalEmptyFolder, mockComponentsGetter, activity, true, user)
-                sut.filter(menu, false)
+                sut = filterFactory.newInstance(normalEmptyFolder, mockComponentsGetter, true, user)
+                toHide = sut.getToHide(false)
 
-                assertFalse(menu.findItem(R.id.action_unset_encrypted).isVisible)
-                assertTrue(menu.findItem(R.id.action_encrypted).isVisible)
+                assertTrue(toHide.contains(R.id.action_unset_encrypted))
+                assertFalse(toHide.contains(R.id.action_encrypted))
             }
         }
     }
 
     private data class ExpectedLockVisibilities(
         val lockFile: Boolean,
-        val unlockFile: Boolean,
-        val lockedBy: Boolean,
-        val lockedUntil: Boolean
+        val unlockFile: Boolean
     )
 
     private fun configureCapability(capability: OCCapability) {
         every { mockStorageManager.getCapability(any<User>()) } returns capability
         every { mockStorageManager.getCapability(any<String>()) } returns capability
-        every { mockComponentsGetter.storageManager } returns mockStorageManager
-    }
-
-    private fun getMenu(activity: TestActivity): Menu {
-        val inflater = activity.menuInflater
-        val menu = MenuBuilder(activity)
-        inflater.inflate(R.menu.item_file, menu)
-        return menu
     }
 
     private fun testLockingVisibilities(
@@ -248,32 +246,20 @@ class FileMenuFilterIT : AbstractIT() {
 
         launchActivity<TestActivity>().use {
             it.onActivity { activity ->
-                val menu = getMenu(activity)
-
-                val sut = FileMenuFilter(file, mockComponentsGetter, activity, true, user)
+                val filterFactory =
+                    FileMenuFilter.Factory(mockStorageManager, activity, editorUtils)
+                val sut = filterFactory.newInstance(file, mockComponentsGetter, true, user)
 
-                sut.filter(menu, false)
+                val toHide = sut.getToHide(false)
 
                 assertEquals(
                     expectedLockVisibilities.lockFile,
-                    menu.findItem(R.id.action_lock_file).isVisible
+                    !toHide.contains(R.id.action_lock_file)
                 )
                 assertEquals(
                     expectedLockVisibilities.unlockFile,
-                    menu.findItem(R.id.action_unlock_file).isVisible
+                    !toHide.contains(R.id.action_unlock_file)
                 )
-                assertEquals(
-                    expectedLockVisibilities.lockedBy,
-                    menu.findItem(R.id.action_locked_by).isVisible
-                )
-                assertEquals(
-                    expectedLockVisibilities.lockedUntil,
-                    menu.findItem(R.id.action_locked_until).isVisible
-                )
-
-                // locked by and until should always be disabled, they're not real actions
-                assertFalse(menu.findItem(R.id.action_locked_by).isEnabled)
-                assertFalse(menu.findItem(R.id.action_locked_until).isEnabled)
             }
         }
     }

+ 13 - 0
app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.java

@@ -44,6 +44,7 @@ import com.nextcloud.client.account.User;
 import com.nextcloud.client.account.UserAccountManager;
 import com.nextcloud.client.device.DeviceInfo;
 import com.nextcloud.ui.ChooseAccountDialogFragment;
+import com.nextcloud.ui.fileactions.FileActionsBottomSheet;
 import com.owncloud.android.AbstractIT;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.datamodel.ArbitraryDataProvider;
@@ -496,6 +497,18 @@ public class DialogFragmentIT extends AbstractIT {
         showDialog(sut);
     }
 
+    @Test
+    @ScreenshotTest
+    public void testFileActionsBottomSheet() {
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
+        }
+
+        OCFile ocFile = new OCFile("/test.md");
+        final FileActionsBottomSheet sut = FileActionsBottomSheet.newInstance(ocFile, false);
+        showDialog(sut);
+    }
+
     private FileDisplayActivity showDialog(DialogFragment dialog) {
         Intent intent = new Intent(targetContext, FileDisplayActivity.class);
 

+ 3 - 1
app/src/debug/java/com/nextcloud/client/TestActivity.kt

@@ -27,8 +27,10 @@ import androidx.fragment.app.Fragment
 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
 import com.nextcloud.client.network.Connectivity
 import com.nextcloud.client.network.ConnectivityService
+import com.nextcloud.utils.EditorUtils
 import com.owncloud.android.R
 import com.owncloud.android.databinding.TestLayoutBinding
+import com.owncloud.android.datamodel.ArbitraryDataProvider
 import com.owncloud.android.datamodel.FileDataStorageManager
 import com.owncloud.android.datamodel.OCFile
 import com.owncloud.android.files.services.FileDownloader
@@ -144,7 +146,7 @@ class TestActivity :
 
     override fun getFileOperationsHelper(): FileOperationsHelper {
         if (!this::fileOperation.isInitialized) {
-            fileOperation = FileOperationsHelper(this, userAccountManager, connectivityServiceMock)
+            fileOperation = FileOperationsHelper(this, userAccountManager, connectivityServiceMock, EditorUtils(ArbitraryDataProvider(contentResolver)))
         }
 
         return fileOperation

+ 0 - 78
app/src/main/java/com/nextcloud/android/files/FileLockingMenuCustomization.kt

@@ -1,78 +0,0 @@
-/*
- * Nextcloud Android client application
- *
- * @author Álvaro Brey Vilas
- * Copyright (C) 2022 Álvaro Brey Vilas
- * Copyright (C) 2022 Nextcloud GmbH
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package com.nextcloud.android.files
-
-import android.content.Context
-import android.graphics.Typeface
-import android.os.Build
-import android.text.style.StyleSpan
-import android.view.Menu
-import android.view.MenuItem
-import com.nextcloud.utils.TimeConstants
-import com.owncloud.android.R
-import com.owncloud.android.datamodel.OCFile
-import com.owncloud.android.lib.resources.files.model.FileLockType
-import com.owncloud.android.utils.DisplayUtils
-
-/**
- * Customizes a Menu to show locking information
- */
-class FileLockingMenuCustomization(val context: Context) {
-    fun customizeMenu(menu: Menu, file: OCFile) {
-        if (file.isLocked) {
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
-                menu.setGroupDividerEnabled(true)
-            }
-            menu.findItem(R.id.action_locked_by).title = getLockedByText(file)
-            showLockedUntil(menu, file)
-        }
-    }
-
-    private fun getLockedByText(file: OCFile): CharSequence {
-        val username = file.lockOwnerDisplayName ?: file.lockOwnerId
-        val resource = when (file.lockType) {
-            FileLockType.COLLABORATIVE -> R.string.locked_by_app
-            else -> R.string.locked_by
-        }
-        return DisplayUtils.createTextWithSpan(
-            context.getString(resource, username),
-            username,
-            StyleSpan(Typeface.BOLD)
-        )
-    }
-
-    private fun showLockedUntil(menu: Menu, file: OCFile) {
-        val lockedUntil: MenuItem = menu.findItem(R.id.action_locked_until)
-        if (file.lockTimestamp == 0L || file.lockTimeout == 0L) {
-            lockedUntil.isVisible = false
-        } else {
-            lockedUntil.title =
-                context.getString(R.string.lock_expiration_info, getExpirationRelativeText(file))
-            lockedUntil.isVisible = true
-        }
-    }
-
-    private fun getExpirationRelativeText(file: OCFile): CharSequence? {
-        val expirationTimestamp = (file.lockTimestamp + file.lockTimeout) * TimeConstants.MILLIS_PER_SECOND
-        return DisplayUtils.getRelativeTimestamp(context, expirationTimestamp, true)
-    }
-}

+ 0 - 42
app/src/main/java/com/nextcloud/android/files/ThemedPopupMenu.kt

@@ -1,42 +0,0 @@
-/*
- * Nextcloud Android client application
- *
- * @author Álvaro Brey Vilas
- * Copyright (C) 2022 Álvaro Brey Vilas
- * Copyright (C) 2022 Nextcloud GmbH
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package com.nextcloud.android.files
-
-import android.content.Context
-import android.view.ContextThemeWrapper
-import android.view.View
-import androidx.appcompat.widget.PopupMenu
-import com.owncloud.android.R
-
-/**
- * This is a [PopupMenu] with grayed out disabled elements
- */
-class ThemedPopupMenu(
-    context: Context,
-    anchor: View
-) : PopupMenu(wrapContext(context), anchor) {
-
-    companion object {
-        private fun wrapContext(context: Context): Context =
-            ContextThemeWrapper(context, R.style.Nextcloud_Widget_PopupMenu)
-    }
-}

+ 4 - 0
app/src/main/java/com/nextcloud/client/di/ComponentsModule.java

@@ -34,6 +34,7 @@ import com.nextcloud.client.widget.DashboardWidgetProvider;
 import com.nextcloud.client.widget.DashboardWidgetService;
 import com.nextcloud.ui.ChooseAccountDialogFragment;
 import com.nextcloud.ui.SetStatusDialogFragment;
+import com.nextcloud.ui.fileactions.FileActionsBottomSheet;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.authentication.AuthenticatorActivity;
 import com.owncloud.android.authentication.DeepLinkLoginActivity;
@@ -462,4 +463,7 @@ abstract class ComponentsModule {
 
     @ContributesAndroidInjector
     abstract SslUntrustedCertDialog sslUntrustedCertDialog();
+
+    @ContributesAndroidInjector
+    abstract FileActionsBottomSheet fileActionsBottomSheet();
 }

+ 6 - 0
app/src/main/java/com/nextcloud/client/di/ViewModelModule.kt

@@ -23,6 +23,7 @@ import androidx.lifecycle.ViewModel
 import androidx.lifecycle.ViewModelProvider
 import com.nextcloud.client.etm.EtmViewModel
 import com.nextcloud.client.logger.ui.LogsViewModel
+import com.nextcloud.ui.fileactions.FileActionsViewModel
 import com.owncloud.android.ui.preview.pdf.PreviewPdfViewModel
 import com.owncloud.android.ui.unifiedsearch.UnifiedSearchViewModel
 import dagger.Binds
@@ -51,6 +52,11 @@ abstract class ViewModelModule {
     @ViewModelKey(PreviewPdfViewModel::class)
     abstract fun previewPDFViewModel(vm: PreviewPdfViewModel): ViewModel
 
+    @Binds
+    @IntoMap
+    @ViewModelKey(FileActionsViewModel::class)
+    abstract fun fileActionsViewModel(vm: FileActionsViewModel): ViewModel
+
     @Binds
     abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
 }

+ 101 - 0
app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt

@@ -0,0 +1,101 @@
+/*
+ * Nextcloud Android client application
+ *
+ *  @author Álvaro Brey
+ *  Copyright (C) 2022 Álvaro Brey
+ *  Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package com.nextcloud.ui.fileactions
+
+import androidx.annotation.DrawableRes
+import androidx.annotation.IdRes
+import androidx.annotation.StringRes
+import com.owncloud.android.R
+
+enum class FileAction(@IdRes val id: Int, @StringRes val title: Int, @DrawableRes val icon: Int? = null) {
+    // selection
+    SELECT_ALL(R.id.action_select_all_action_menu, R.string.select_all, R.drawable.ic_select_all),
+    SELECT_NONE(R.id.action_deselect_all_action_menu, R.string.deselect_all, R.drawable.ic_select_none),
+
+    // generic file actions
+    EDIT(R.id.action_edit, R.string.action_edit, R.drawable.ic_edit),
+    SEE_DETAILS(R.id.action_see_details, R.string.actionbar_see_details, R.drawable.ic_information_outline),
+    REMOVE_FILE(R.id.action_remove_file, R.string.common_remove, R.drawable.ic_delete),
+
+    // File moving
+    RENAME_FILE(R.id.action_rename_file, R.string.common_rename, R.drawable.ic_rename),
+    MOVE(R.id.action_move, R.string.actionbar_move, R.drawable.ic_move),
+    COPY(R.id.action_copy, R.string.actionbar_copy, R.drawable.ic_content_copy),
+
+    // favorites
+    FAVORITE(R.id.action_favorite, R.string.favorite, R.drawable.ic_star),
+    UNSET_FAVORITE(R.id.action_unset_favorite, R.string.unset_favorite, R.drawable.ic_star_outline),
+
+    // Uploads and downloads
+    DOWNLOAD_FILE(R.id.action_download_file, R.string.filedetails_download, R.drawable.ic_cloud_download),
+    SYNC_FILE(R.id.action_sync_file, R.string.filedetails_sync_file, R.drawable.ic_cloud_sync_on),
+    CANCEL_SYNC(R.id.action_cancel_sync, R.string.common_cancel_sync, R.drawable.ic_cloud_sync_off),
+
+    // File sharing
+    EXPORT_FILE(R.id.action_export_file, R.string.filedetails_export, R.drawable.ic_export),
+    SEND_SHARE_FILE(R.id.action_send_share_file, R.string.action_send_share, R.drawable.ic_share),
+    SEND_FILE(R.id.action_send_file, R.string.common_send, R.drawable.ic_share),
+    OPEN_FILE_WITH(R.id.action_open_file_with, R.string.actionbar_open_with, R.drawable.ic_external),
+    STREAM_MEDIA(R.id.action_stream_media, R.string.stream, R.drawable.ic_play_arrow),
+    SET_AS_WALLPAPER(R.id.action_set_as_wallpaper, R.string.set_picture_as, R.drawable.ic_wallpaper),
+
+    // Encryption
+    SET_ENCRYPTED(R.id.action_encrypted, R.string.encrypted, R.drawable.ic_encrypt),
+    UNSET_ENCRYPTED(R.id.action_unset_encrypted, R.string.unset_encrypted, R.drawable.ic_decrypt),
+
+    // locks
+    UNLOCK_FILE(R.id.action_unlock_file, R.string.unlock_file, R.drawable.ic_lock_open_white),
+    LOCK_FILE(R.id.action_lock_file, R.string.lock_file, R.drawable.ic_lock);
+
+    companion object {
+        /**
+         * All file actions, in the order they should be displayed
+         */
+        @JvmField
+        val SORTED_VALUES = listOf(
+            UNLOCK_FILE,
+            EDIT,
+            FAVORITE,
+            UNSET_FAVORITE,
+            SEE_DETAILS,
+            LOCK_FILE,
+            RENAME_FILE,
+            MOVE,
+            COPY,
+            DOWNLOAD_FILE,
+            EXPORT_FILE,
+            STREAM_MEDIA,
+            SEND_SHARE_FILE,
+            SEND_FILE,
+            OPEN_FILE_WITH,
+            SYNC_FILE,
+            CANCEL_SYNC,
+            SELECT_ALL,
+            SELECT_NONE,
+            SET_ENCRYPTED,
+            UNSET_ENCRYPTED,
+            SET_AS_WALLPAPER,
+            REMOVE_FILE
+        )
+    }
+}

+ 338 - 0
app/src/main/java/com/nextcloud/ui/fileactions/FileActionsBottomSheet.kt

@@ -0,0 +1,338 @@
+/*
+ * Nextcloud Android client application
+ *
+ *  @author Álvaro Brey
+ *  Copyright (C) 2022 Álvaro Brey
+ *  Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package com.nextcloud.ui.fileactions
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.graphics.Typeface
+import android.graphics.drawable.Drawable
+import android.os.Bundle
+import android.text.style.StyleSpan
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+import androidx.annotation.IdRes
+import androidx.appcompat.content.res.AppCompatResources
+import androidx.core.os.bundleOf
+import androidx.core.view.isVisible
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.setFragmentResult
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ViewModelProvider
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import com.nextcloud.android.common.ui.theme.utils.ColorRole
+import com.nextcloud.client.account.CurrentAccountProvider
+import com.nextcloud.client.di.Injectable
+import com.nextcloud.client.di.ViewModelFactory
+import com.owncloud.android.R
+import com.owncloud.android.databinding.FileActionsBottomSheetBinding
+import com.owncloud.android.databinding.FileActionsBottomSheetItemBinding
+import com.owncloud.android.datamodel.FileDataStorageManager
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.datamodel.ThumbnailsCacheManager
+import com.owncloud.android.lib.resources.files.model.FileLockType
+import com.owncloud.android.ui.activity.ComponentsGetter
+import com.owncloud.android.utils.DisplayUtils
+import com.owncloud.android.utils.DisplayUtils.AvatarGenerationListener
+import com.owncloud.android.utils.theme.ViewThemeUtils
+import javax.inject.Inject
+
+class FileActionsBottomSheet private constructor() : BottomSheetDialogFragment(), Injectable {
+
+    @Inject
+    lateinit var viewThemeUtils: ViewThemeUtils
+
+    @Inject
+    lateinit var vmFactory: ViewModelFactory
+
+    @Inject
+    lateinit var currentUserProvider: CurrentAccountProvider
+
+    @Inject
+    lateinit var storageManager: FileDataStorageManager
+
+    lateinit var viewModel: FileActionsViewModel
+
+    private var _binding: FileActionsBottomSheetBinding? = null
+    private val binding
+        get() = _binding!!
+
+    lateinit var componentsGetter: ComponentsGetter
+
+    private val thumbnailAsyncTasks = mutableListOf<ThumbnailsCacheManager.ThumbnailGenerationTask>()
+
+    interface ResultListener {
+        fun onResult(@IdRes actionId: Int)
+    }
+
+    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+        viewModel = ViewModelProvider(this, vmFactory)[FileActionsViewModel::class.java]
+        _binding = FileActionsBottomSheetBinding.inflate(inflater, container, false)
+
+        viewModel.uiState.observe(viewLifecycleOwner, this::handleState)
+
+        viewModel.clickActionId.observe(viewLifecycleOwner) { id ->
+            dispatchActionClick(id)
+        }
+
+        viewModel.load(requireArguments(), componentsGetter)
+
+        return binding.root
+    }
+
+    private fun handleState(
+        state: FileActionsViewModel.UiState
+    ) {
+        toggleLoadingOrContent(state)
+        when (state) {
+            is FileActionsViewModel.UiState.LoadedForSingleFile -> {
+                loadFileThumbnail(state.titleFile)
+                if (state.lockInfo != null) {
+                    displayLockInfo(state.lockInfo)
+                }
+                displayActions(state.actions)
+                displayTitle(state.titleFile)
+            }
+            is FileActionsViewModel.UiState.LoadedForMultipleFiles -> {
+                setMultipleFilesThumbnail()
+                displayActions(state.actions)
+                displayTitle(state.fileCount)
+            }
+            FileActionsViewModel.UiState.Loading -> {}
+            FileActionsViewModel.UiState.Error -> {
+                context?.let {
+                    Toast.makeText(it, R.string.error_file_actions, Toast.LENGTH_SHORT).show()
+                }
+                dismissAllowingStateLoss()
+            }
+        }
+    }
+
+    private fun loadFileThumbnail(titleFile: OCFile?) {
+        titleFile?.let {
+            DisplayUtils.setThumbnail(
+                it,
+                binding.thumbnailLayout.thumbnail,
+                currentUserProvider.user,
+                storageManager,
+                thumbnailAsyncTasks,
+                false,
+                context,
+                binding.thumbnailLayout.thumbnailShimmer,
+                null,
+                viewThemeUtils
+            )
+        }
+    }
+
+    private fun setMultipleFilesThumbnail() {
+        context?.let {
+            val drawable = viewThemeUtils.platform.tintDrawable(it, R.drawable.file_multiple, ColorRole.PRIMARY)
+            binding.thumbnailLayout.thumbnail.setImageDrawable(drawable)
+        }
+    }
+
+    override fun onDestroyView() {
+        super.onDestroyView()
+        _binding = null
+    }
+
+    override fun onAttach(context: Context) {
+        super.onAttach(context)
+        require(context is ComponentsGetter) {
+            "Context is not a ComponentsGetter"
+        }
+        this.componentsGetter = context
+    }
+
+    fun setResultListener(
+        fragmentManager: FragmentManager,
+        lifecycleOwner: LifecycleOwner,
+        listener: ResultListener
+    ): FileActionsBottomSheet {
+        fragmentManager.setFragmentResultListener(REQUEST_KEY, lifecycleOwner) { _, result ->
+            @IdRes val actionId = result.getInt(RESULT_KEY_ACTION_ID, -1)
+            if (actionId != -1) {
+                listener.onResult(actionId)
+            }
+        }
+        return this
+    }
+
+    private fun toggleLoadingOrContent(state: FileActionsViewModel.UiState) {
+        if (state is FileActionsViewModel.UiState.Loading) {
+            binding.bottomSheetLoading.isVisible = true
+            binding.bottomSheetContent.isVisible = false
+            viewThemeUtils.platform.colorCircularProgressBar(binding.bottomSheetLoading)
+        } else {
+            binding.bottomSheetLoading.isVisible = false
+            binding.bottomSheetContent.isVisible = true
+        }
+    }
+
+    private fun displayActions(
+        actions: List<FileAction>
+    ) {
+        actions.forEach { action ->
+            val view = inflateActionView(action)
+            binding.fileActionsList.addView(view)
+        }
+    }
+
+    private fun displayTitle(titleFile: OCFile?) {
+        val decryptedFileName = titleFile?.decryptedFileName
+        if (decryptedFileName != null) {
+            decryptedFileName.let {
+                binding.title.text = it
+            }
+        } else {
+            binding.title.isVisible = false
+        }
+    }
+
+    private fun displayLockInfo(lockInfo: FileActionsViewModel.LockInfo) {
+        val view = FileActionsBottomSheetItemBinding.inflate(layoutInflater, binding.fileActionsList, false)
+            .apply {
+                val textColor = ColorStateList.valueOf(resources.getColor(R.color.secondary_text_color, null))
+                root.isClickable = false
+                text.setTextColor(textColor)
+                text.text = getLockedByText(lockInfo)
+                if (lockInfo.lockedUntil != null) {
+                    textLine2.text = getLockedUntilText(lockInfo)
+                    textLine2.isVisible = true
+                }
+                if (lockInfo.lockType != FileLockType.COLLABORATIVE) {
+                    showLockAvatar(lockInfo)
+                }
+            }
+        binding.fileActionsList.addView(view.root)
+    }
+
+    private fun FileActionsBottomSheetItemBinding.showLockAvatar(lockInfo: FileActionsViewModel.LockInfo) {
+        val listener = object : AvatarGenerationListener {
+            override fun avatarGenerated(avatarDrawable: Drawable?, callContext: Any?) {
+                icon.setImageDrawable(avatarDrawable)
+            }
+
+            override fun shouldCallGeneratedCallback(tag: String?, callContext: Any?): Boolean {
+                return false
+            }
+        }
+        DisplayUtils.setAvatar(
+            currentUserProvider.user,
+            lockInfo.lockedBy,
+            listener,
+            resources.getDimension(R.dimen.list_item_avatar_icon_radius),
+            resources,
+            this,
+            requireContext()
+        )
+    }
+
+    private fun getLockedByText(lockInfo: FileActionsViewModel.LockInfo): CharSequence {
+        val resource = when (lockInfo.lockType) {
+            FileLockType.COLLABORATIVE -> R.string.locked_by_app
+            else -> R.string.locked_by
+        }
+        return DisplayUtils.createTextWithSpan(
+            getString(resource, lockInfo.lockedBy),
+            lockInfo.lockedBy,
+            StyleSpan(Typeface.BOLD)
+        )
+    }
+
+    private fun getLockedUntilText(lockInfo: FileActionsViewModel.LockInfo): CharSequence {
+        val relativeTimestamp = DisplayUtils.getRelativeTimestamp(context, lockInfo.lockedUntil!!, true)
+        return getString(R.string.lock_expiration_info, relativeTimestamp)
+    }
+
+    private fun displayTitle(fileCount: Int) {
+        binding.title.text = resources.getQuantityString(R.plurals.file_list__footer__file, fileCount, fileCount)
+    }
+
+    private fun inflateActionView(action: FileAction): View {
+        val itemBinding = FileActionsBottomSheetItemBinding.inflate(layoutInflater, binding.fileActionsList, false)
+            .apply {
+                root.setOnClickListener {
+                    viewModel.onClick(action)
+                }
+                text.setText(action.title)
+                if (action.icon != null) {
+                    val drawable =
+                        viewThemeUtils.platform.tintDrawable(
+                            requireContext(),
+                            AppCompatResources.getDrawable(requireContext(), action.icon)!!
+                        )
+                    icon.setImageDrawable(drawable)
+                }
+            }
+        return itemBinding.root
+    }
+
+    private fun dispatchActionClick(id: Int?) {
+        if (id != null) {
+            setFragmentResult(REQUEST_KEY, bundleOf(RESULT_KEY_ACTION_ID to id))
+            parentFragmentManager.clearFragmentResultListener(REQUEST_KEY)
+            dismiss()
+        }
+    }
+
+    companion object {
+        private const val REQUEST_KEY = "REQUEST_KEY_ACTION"
+        private const val RESULT_KEY_ACTION_ID = "RESULT_KEY_ACTION_ID"
+
+        @JvmStatic
+        @JvmOverloads
+        fun newInstance(
+            file: OCFile,
+            isOverflow: Boolean,
+            @IdRes
+            additionalToHide: List<Int>? = null
+        ): FileActionsBottomSheet {
+            return newInstance(1, listOf(file), isOverflow, additionalToHide)
+        }
+
+        @JvmStatic
+        @JvmOverloads
+        fun newInstance(
+            numberOfAllFiles: Int,
+            files: Collection<OCFile>,
+            isOverflow: Boolean,
+            @IdRes
+            additionalToHide: List<Int>? = null
+        ): FileActionsBottomSheet {
+            return FileActionsBottomSheet().apply {
+                val argsBundle = bundleOf(
+                    FileActionsViewModel.ARG_ALL_FILES_COUNT to numberOfAllFiles,
+                    FileActionsViewModel.ARG_FILES to ArrayList<OCFile>(files),
+                    FileActionsViewModel.ARG_IS_OVERFLOW to isOverflow
+                )
+                additionalToHide?.let {
+                    argsBundle.putIntArray(FileActionsViewModel.ARG_ADDITIONAL_FILTER, additionalToHide.toIntArray())
+                }
+                arguments = argsBundle
+            }
+        }
+    }
+}

+ 167 - 0
app/src/main/java/com/nextcloud/ui/fileactions/FileActionsViewModel.kt

@@ -0,0 +1,167 @@
+/*
+ * Nextcloud Android client application
+ *
+ *  @author Álvaro Brey
+ *  Copyright (C) 2022 Álvaro Brey
+ *  Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package com.nextcloud.ui.fileactions
+
+import android.os.Bundle
+import androidx.annotation.IdRes
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.nextcloud.client.account.CurrentAccountProvider
+import com.nextcloud.client.logger.Logger
+import com.nextcloud.utils.TimeConstants
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.files.FileMenuFilter
+import com.owncloud.android.lib.resources.files.model.FileLockType
+import com.owncloud.android.ui.activity.ComponentsGetter
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+class FileActionsViewModel @Inject constructor(
+    private val currentAccountProvider: CurrentAccountProvider,
+    private val filterFactory: FileMenuFilter.Factory,
+    private val logger: Logger
+) :
+    ViewModel() {
+
+    data class LockInfo(val lockType: FileLockType, val lockedBy: String, val lockedUntil: Long?)
+
+    sealed interface UiState {
+        object Loading : UiState
+        object Error : UiState
+        data class LoadedForSingleFile(
+            val actions: List<FileAction>,
+            val titleFile: OCFile?,
+            val lockInfo: LockInfo? = null
+        ) : UiState
+
+        data class LoadedForMultipleFiles(val actions: List<FileAction>, val fileCount: Int) : UiState
+    }
+
+    private val _uiState: MutableLiveData<UiState> = MutableLiveData(UiState.Loading)
+    val uiState: LiveData<UiState>
+        get() = _uiState
+
+    private val _clickActionId: MutableLiveData<Int?> = MutableLiveData(null)
+    val clickActionId: LiveData<Int?>
+        @IdRes
+        get() = _clickActionId
+
+    fun load(arguments: Bundle, componentsGetter: ComponentsGetter) {
+        val files: List<OCFile>? = arguments.getParcelableArrayList(ARG_FILES)
+        val numberOfAllFiles: Int = arguments.getInt(ARG_ALL_FILES_COUNT, 1)
+        val isOverflow = arguments.getBoolean(ARG_IS_OVERFLOW, false)
+        val additionalFilter: IntArray? = arguments.getIntArray(ARG_ADDITIONAL_FILTER)
+
+        if (files.isNullOrEmpty()) {
+            logger.d(TAG, "No valid files argument for loading actions")
+            _uiState.postValue(UiState.Error)
+        } else {
+            load(componentsGetter, files.toList(), numberOfAllFiles, isOverflow, additionalFilter)
+        }
+    }
+
+    private fun load(
+        componentsGetter: ComponentsGetter,
+        files: Collection<OCFile>,
+        numberOfAllFiles: Int?,
+        isOverflow: Boolean?,
+        additionalFilter: IntArray?
+    ) {
+        viewModelScope.launch(Dispatchers.IO) {
+            val toHide = getHiddenActions(componentsGetter, numberOfAllFiles, files, isOverflow)
+            val availableActions = getActionsToShow(additionalFilter, toHide)
+            updateStateLoaded(files, availableActions)
+        }
+    }
+
+    private fun getHiddenActions(
+        componentsGetter: ComponentsGetter,
+        numberOfAllFiles: Int?,
+        files: Collection<OCFile>,
+        isOverflow: Boolean?
+    ): List<Int> {
+        return filterFactory.newInstance(
+            numberOfAllFiles ?: 1,
+            files.toList(),
+            componentsGetter,
+            isOverflow ?: false,
+            currentAccountProvider.user
+        )
+            .getToHide(false)
+    }
+
+    private fun getActionsToShow(
+        additionalFilter: IntArray?,
+        toHide: List<Int>
+    ) = FileAction.SORTED_VALUES
+        .filter { additionalFilter == null || it.id !in additionalFilter }
+        .filter { it.id !in toHide }
+
+    private fun updateStateLoaded(
+        files: Collection<OCFile>,
+        availableActions: List<FileAction>
+    ) {
+        val state: UiState = when (files.size) {
+            1 -> {
+                val file = files.first()
+                UiState.LoadedForSingleFile(availableActions, file, getLockInfo(file))
+            }
+            else -> UiState.LoadedForMultipleFiles(availableActions, files.size)
+        }
+        _uiState.postValue(state)
+    }
+
+    private fun getLockInfo(file: OCFile): LockInfo? {
+        val lockType = file.lockType
+        val username = file.lockOwnerDisplayName ?: file.lockOwnerId
+        return if (file.isLocked && lockType != null && username != null) {
+            LockInfo(lockType, username, getLockedUntil(file))
+        } else {
+            null
+        }
+    }
+
+    private fun getLockedUntil(file: OCFile): Long? {
+        return if (file.lockTimestamp == 0L || file.lockTimeout == 0L) {
+            null
+        } else {
+            (file.lockTimestamp + file.lockTimeout) * TimeConstants.MILLIS_PER_SECOND
+        }
+    }
+
+    fun onClick(action: FileAction) {
+        _clickActionId.value = action.id
+    }
+
+    companion object {
+        const val ARG_ALL_FILES_COUNT = "ALL_FILES_COUNT"
+        const val ARG_FILES = "FILES"
+        const val ARG_IS_OVERFLOW = "OVERFLOW"
+        const val ARG_ADDITIONAL_FILTER = "ADDITIONAL_FILTER"
+
+        private val TAG = FileActionsViewModel::class.simpleName!!
+    }
+}

+ 47 - 0
app/src/main/java/com/nextcloud/utils/EditorUtils.kt

@@ -0,0 +1,47 @@
+/*
+ * Nextcloud Android client application
+ *
+ *  @author Álvaro Brey
+ *  Copyright (C) 2022 Álvaro Brey
+ *  Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package com.nextcloud.utils
+
+import com.google.gson.Gson
+import com.nextcloud.client.account.User
+import com.owncloud.android.datamodel.ArbitraryDataProvider
+import com.owncloud.android.lib.common.DirectEditing
+import com.owncloud.android.lib.common.Editor
+import javax.inject.Inject
+
+class EditorUtils @Inject constructor(private val arbitraryDataProvider: ArbitraryDataProvider) {
+
+    fun getEditor(user: User?, mimeType: String?): Editor? {
+        val json = arbitraryDataProvider.getValue(user, ArbitraryDataProvider.DIRECT_EDITING)
+        if (json.isEmpty()) {
+            return null
+        }
+        val editors = Gson().fromJson(json, DirectEditing::class.java).editors.values
+        return editors.firstOrNull { mimeType in it.mimetypes }
+            ?: editors.firstOrNull { mimeType in it.optionalMimetypes }
+    }
+
+    fun isEditorAvailable(user: User?, mimeType: String?): Boolean {
+        return getEditor(user, mimeType) != null
+    }
+}

+ 55 - 0
app/src/main/java/com/nextcloud/utils/MenuUtils.kt

@@ -0,0 +1,55 @@
+/*
+ * Nextcloud Android client application
+ *
+ *  @author Álvaro Brey
+ *  Copyright (C) 2022 Álvaro Brey
+ *  Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+package com.nextcloud.utils
+
+import android.view.Menu
+import android.view.MenuItem
+import androidx.core.view.children
+
+object MenuUtils {
+
+    @JvmStatic
+    fun showMenuItem(item: MenuItem?) {
+        item?.apply {
+            isVisible = true
+            isEnabled = true
+        }
+    }
+
+    @JvmStatic
+    fun hideMenuItem(item: MenuItem?) {
+        item?.apply {
+            isVisible = false
+            isEnabled = false
+        }
+    }
+
+    @JvmStatic
+    fun hideAll(menu: Menu?) {
+        menu?.children?.forEach(::hideMenuItem)
+    }
+
+    @JvmStatic
+    fun hideMenuItems(vararg items: MenuItem?) {
+        items.filterNotNull().forEach(::hideMenuItem)
+    }
+}

+ 128 - 267
app/src/main/java/com/owncloud/android/files/FileMenuFilter.java

@@ -3,8 +3,11 @@
  *
  * @author David A. Velasco
  * @author Andy Scherzinger
+ * @author Álvaro Brey
  * Copyright (C) 2015 ownCloud Inc.
  * Copyright (C) 2018 Andy Scherzinger
+ * Copyright (C) 2022 Álvaro Brey Vilas
+ * Copyright (C) 2022 Nextcloud GmbH
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License version 2,
@@ -22,21 +25,17 @@
 package com.owncloud.android.files;
 
 import android.accounts.AccountManager;
-import android.content.ContentResolver;
 import android.content.Context;
 import android.view.Menu;
-import android.view.MenuItem;
 
-import com.google.gson.Gson;
 import com.nextcloud.android.files.FileLockingHelper;
 import com.nextcloud.client.account.User;
+import com.nextcloud.utils.EditorUtils;
 import com.owncloud.android.R;
-import com.owncloud.android.datamodel.ArbitraryDataProvider;
+import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder;
 import com.owncloud.android.files.services.FileUploader.FileUploaderBinder;
-import com.owncloud.android.lib.common.DirectEditing;
-import com.owncloud.android.lib.common.Editor;
 import com.owncloud.android.lib.resources.status.OCCapability;
 import com.owncloud.android.services.OperationsService.OperationsServiceBinder;
 import com.owncloud.android.ui.activity.ComponentsGetter;
@@ -49,7 +48,9 @@ import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 
-import androidx.annotation.Nullable;
+import javax.inject.Inject;
+
+import androidx.annotation.IdRes;
 
 /**
  * Filters out the file actions available in a given {@link Menu} for a given {@link OCFile}
@@ -68,24 +69,54 @@ public class FileMenuFilter {
     private final boolean overflowMenu;
     private final User user;
     private final String userId;
-
-    /**
-     * Constructor
-     *
-     * @param numberOfAllFiles  Number of all displayed files
-     * @param files             Collection of {@link OCFile} file targets of the action to filter in the {@link Menu}.
-     * @param componentsGetter  Accessor to app components, needed to access synchronization services
-     * @param context           Android {@link Context}, needed to access build setup resources.
-     * @param overflowMenu      true if the overflow menu items are being filtered
-     * @param user              currently active user
-     */
-    public FileMenuFilter(int numberOfAllFiles,
-                          Collection<OCFile> files,
-                          ComponentsGetter componentsGetter,
-                          Context context,
-                          boolean overflowMenu,
-                          User user
-    ) {
+    private final FileDataStorageManager storageManager;
+    private final EditorUtils editorUtils;
+
+
+    public static class Factory {
+        private final FileDataStorageManager storageManager;
+        private final Context context;
+        private final EditorUtils editorUtils;
+
+        @Inject
+        public Factory(final FileDataStorageManager storageManager, final Context context, final EditorUtils editorUtils) {
+            this.storageManager = storageManager;
+            this.context = context;
+            this.editorUtils = editorUtils;
+        }
+
+        /**
+         * @param numberOfAllFiles Number of all displayed files
+         * @param files            Collection of {@link OCFile} file targets of the action to filter in the {@link Menu}.
+         * @param componentsGetter Accessor to app components, needed to access synchronization services
+         * @param overflowMenu     true if the overflow menu items are being filtered
+         * @param user             currently active user
+         */
+        public FileMenuFilter newInstance(final int numberOfAllFiles, final Collection<OCFile> files, final ComponentsGetter componentsGetter, boolean overflowMenu, User user) {
+            return new FileMenuFilter(storageManager, editorUtils, numberOfAllFiles, files, componentsGetter, context, overflowMenu, user);
+        }
+
+        /**
+         * @param file             {@link OCFile} file target
+         * @param componentsGetter Accessor to app components, needed to access synchronization services
+         * @param overflowMenu     true if the overflow menu items are being filtered
+         * @param user             currently active user
+         */
+        public FileMenuFilter newInstance(final OCFile file, final ComponentsGetter componentsGetter, boolean overflowMenu, User user) {
+            return newInstance(1, Collections.singletonList(file), componentsGetter, overflowMenu, user);
+        }
+    }
+
+
+    private FileMenuFilter(FileDataStorageManager storageManager, EditorUtils editorUtils, int numberOfAllFiles,
+                           Collection<OCFile> files,
+                           ComponentsGetter componentsGetter,
+                           Context context,
+                           boolean overflowMenu,
+                           User user
+                          ) {
+        this.storageManager = storageManager;
+        this.editorUtils = editorUtils;
         this.numberOfAllFiles = numberOfAllFiles;
         this.files = files;
         this.componentsGetter = componentsGetter;
@@ -99,147 +130,66 @@ public class FileMenuFilter {
     }
 
     /**
-     * Constructor
-     *
-     * @param file              {@link OCFile} target of the action to filter in the {@link Menu}.
-     * @param componentsGetter  Accessor to app components, needed to access synchronization services
-     * @param context           Android {@link Context}, needed to access build setup resources.
-     * @param overflowMenu      true if the overflow menu items are being filtered
-     * @param user              currently active user
-     */
-    public FileMenuFilter(OCFile file,
-                          ComponentsGetter componentsGetter,
-                          Context context,
-                          boolean overflowMenu,
-                          User user
-    ) {
-        this(1, Collections.singletonList(file), componentsGetter, context, overflowMenu, user);
-    }
-
-    /**
-     * Filters out the file actions available in the passed {@link Menu} taken into account the state of the {@link
-     * OCFile} held by the filter.
-     *
-     * @param menu                 Options or context menu to filter.
-     * @param inSingleFileFragment True if this is not listing, but single file fragment, like preview or details.
+     * List of actions to remove given the parameters supplied in the constructor
      */
-    public void filter(Menu menu, boolean inSingleFileFragment) {
-        if (files == null || files.isEmpty()) {
-            hideAll(menu);
-        } else {
-            List<Integer> toShow = new ArrayList<>();
-            List<Integer> toHide = new ArrayList<>();
-
-            filter(toShow, toHide, inSingleFileFragment);
-
-            for (int i : toShow) {
-                final MenuItem item = menu.findItem(i);
-                if (item != null) {
-                    showMenuItem(item);
-                } else {
-                    // group
-                    menu.setGroupVisible(i, true);
-                }
-            }
-
-            for (int i : toHide) {
-                final MenuItem item = menu.findItem(i);
-                if (item != null) {
-                    hideMenuItem(item);
-                } else {
-                    // group
-                    menu.setGroupVisible(i, false);
-                }
-            }
-        }
-    }
-
-    public static void hideAll(Menu menu) {
-        if (menu != null) {
-            for (int i = 0; i < menu.size(); i++) {
-                hideMenuItem(menu.getItem(i));
-            }
-        }
-    }
-
-    /**
-     * hides a given {@link MenuItem}.
-     *
-     * @param item the {@link MenuItem} to be hidden
-     */
-    public static void hideMenuItem(MenuItem item) {
-        if (item != null) {
-            item.setVisible(false);
-            item.setEnabled(false);
+    @IdRes
+    public List<Integer> getToHide(final boolean inSingleFileFragment){
+        if(files != null && ! files.isEmpty()){
+            return filter(inSingleFileFragment);
         }
+        return null;
     }
 
-    private static void showMenuItem(MenuItem item) {
-        if (item != null) {
-            item.setVisible(true);
-            item.setEnabled(true);
-        }
-    }
-
-    public static void hideMenuItems(MenuItem... items) {
-        if (items != null) {
-            for (MenuItem item : items) {
-                hideMenuItem(item);
-            }
-        }
-    }
 
     /**
      * Decides what actions must be shown and hidden implementing the different rule sets.
-     *  @param toShow                List to save the options that must be shown in the menu.
-     * @param toHide                List to save the options that must be shown in the menu.
-     * @param inSingleFileFragment  True if this is not listing, but single file fragment, like preview or details.
+     *
+     * @param inSingleFileFragment True if this is not listing, but single file fragment, like preview or details.
      */
-    private void filter(List<Integer> toShow,
-                        List<Integer> toHide,
-                        boolean inSingleFileFragment) {
+    private List<Integer> filter(boolean inSingleFileFragment) {
         boolean synchronizing = anyFileSynchronizing();
-        OCCapability capability = componentsGetter.getStorageManager().getCapability(user.getAccountName());
+        OCCapability capability = storageManager.getCapability(user.getAccountName());
         boolean endToEndEncryptionEnabled = capability.getEndToEndEncryption().isTrue();
         boolean fileLockingEnabled = capability.getFilesLockingVersion() != null;
 
-        filterEdit(toShow, toHide, capability);
-        filterDownload(toShow, toHide, synchronizing);
-        filterExport(toShow, toHide);
-        filterRename(toShow, toHide, synchronizing);
-        filterCopy(toShow, toHide, synchronizing);
-        filterMove(toShow, toHide, synchronizing);
-        filterRemove(toShow, toHide, synchronizing);
-        filterSelectAll(toShow, toHide, inSingleFileFragment);
-        filterDeselectAll(toShow, toHide, inSingleFileFragment);
-        filterOpenWith(toShow, toHide, synchronizing);
-        filterCancelSync(toShow, toHide, synchronizing);
-        filterSync(toShow, toHide, synchronizing);
-        filterShareFile(toShow, toHide, capability);
-        filterSendFiles(toShow, toHide, inSingleFileFragment);
-        filterDetails(toShow, toHide);
-        filterFavorite(toShow, toHide, synchronizing);
-        filterUnfavorite(toShow, toHide, synchronizing);
-        filterEncrypt(toShow, toHide, endToEndEncryptionEnabled);
-        filterUnsetEncrypted(toShow, toHide, endToEndEncryptionEnabled);
-        filterSetPictureAs(toShow, toHide);
-        filterStream(toShow, toHide);
-        filterLock(toShow, toHide, fileLockingEnabled);
-        filterUnlock(toShow, toHide, fileLockingEnabled);
-        filterLockInfo(toShow, toHide, fileLockingEnabled);
-    }
-
-    private void filterShareFile(List<Integer> toShow, List<Integer> toHide, OCCapability capability) {
+        @IdRes final List<Integer> toHide = new ArrayList<>();
+
+        filterEdit(toHide, capability);
+        filterDownload(toHide, synchronizing);
+        filterExport(toHide);
+        filterRename(toHide, synchronizing);
+        filterCopy(toHide, synchronizing);
+        filterMove(toHide, synchronizing);
+        filterRemove(toHide, synchronizing);
+        filterSelectAll(toHide, inSingleFileFragment);
+        filterDeselectAll(toHide, inSingleFileFragment);
+        filterOpenWith(toHide, synchronizing);
+        filterCancelSync(toHide, synchronizing);
+        filterSync(toHide, synchronizing);
+        filterShareFile(toHide, capability);
+        filterSendFiles(toHide, inSingleFileFragment);
+        filterDetails(toHide);
+        filterFavorite(toHide, synchronizing);
+        filterUnfavorite(toHide, synchronizing);
+        filterEncrypt(toHide, endToEndEncryptionEnabled);
+        filterUnsetEncrypted(toHide, endToEndEncryptionEnabled);
+        filterSetPictureAs(toHide);
+        filterStream(toHide);
+        filterLock(toHide, fileLockingEnabled);
+        filterUnlock(toHide, fileLockingEnabled);
+
+        return toHide;
+    }
+
+    private void filterShareFile(List<Integer> toHide, OCCapability capability) {
         if (containsEncryptedFile() || (!isShareViaLinkAllowed() && !isShareWithUsersAllowed()) ||
             !isSingleSelection() || !isShareApiEnabled(capability) || !files.iterator().next().canReshare()
             || overflowMenu) {
             toHide.add(R.id.action_send_share_file);
-        } else {
-            toShow.add(R.id.action_send_share_file);
         }
     }
 
-    private void filterSendFiles(List<Integer> toShow, List<Integer> toHide, boolean inSingleFileFragment) {
+    private void filterSendFiles(List<Integer> toHide, boolean inSingleFileFragment) {
         boolean show = true;
         if (overflowMenu || SEND_OFF.equalsIgnoreCase(context.getString(R.string.send_files_to_other_apps)) || containsEncryptedFile()) {
             show = false;
@@ -247,106 +197,75 @@ public class FileMenuFilter {
         if (!inSingleFileFragment && (isSingleSelection() || !anyFileDown())) {
             show = false;
         }
-        if (show) {
-            toShow.add(R.id.action_send_file);
-        } else {
+        if (!show) {
             toHide.add(R.id.action_send_file);
         }
     }
 
-    private void filterDetails(Collection<Integer> toShow, Collection<Integer> toHide) {
-        if (isSingleSelection()) {
-            toShow.add(R.id.action_see_details);
-        } else {
+    private void filterDetails(Collection<Integer> toHide) {
+        if (!isSingleSelection()) {
             toHide.add(R.id.action_see_details);
         }
     }
 
-    private void filterFavorite(List<Integer> toShow, List<Integer> toHide, boolean synchronizing) {
+    private void filterFavorite(List<Integer> toHide, boolean synchronizing) {
         if (files.isEmpty() || synchronizing || allFavorites()) {
             toHide.add(R.id.action_favorite);
-        } else {
-            toShow.add(R.id.action_favorite);
         }
     }
 
-    private void filterUnfavorite(List<Integer> toShow, List<Integer> toHide, boolean synchronizing) {
+    private void filterUnfavorite(List<Integer> toHide, boolean synchronizing) {
         if (files.isEmpty() || synchronizing || allNotFavorites()) {
             toHide.add(R.id.action_unset_favorite);
-        } else {
-            toShow.add(R.id.action_unset_favorite);
         }
     }
 
-    private void filterLock(List<Integer> toShow, List<Integer> toHide, boolean fileLockingEnabled) {
+    private void filterLock(List<Integer> toHide, boolean fileLockingEnabled) {
         if (files.isEmpty() || !isSingleSelection() || !fileLockingEnabled) {
             toHide.add(R.id.action_lock_file);
         } else {
             OCFile file = files.iterator().next();
             if (file.isLocked() || file.isFolder()) {
                 toHide.add(R.id.action_lock_file);
-            } else {
-                toShow.add(R.id.action_lock_file);
             }
         }
     }
 
-    private void filterUnlock(List<Integer> toShow, List<Integer> toHide, boolean fileLockingEnabled) {
+    private void filterUnlock(List<Integer> toHide, boolean fileLockingEnabled) {
         if (files.isEmpty() || !isSingleSelection() || !fileLockingEnabled) {
             toHide.add(R.id.action_unlock_file);
         } else {
             OCFile file = files.iterator().next();
-            if (FileLockingHelper.canUserUnlockFile(userId, file)) {
-                toShow.add(R.id.action_unlock_file);
-            } else {
+            if (!FileLockingHelper.canUserUnlockFile(userId, file)) {
                 toHide.add(R.id.action_unlock_file);
             }
         }
     }
 
-    private void filterLockInfo(List<Integer> toShow, List<Integer> toHide, boolean fileLockingEnabled) {
-        if (files.isEmpty() || !isSingleSelection() || !fileLockingEnabled) {
-            toHide.add(R.id.menu_group_lock_info);
-        } else {
-            OCFile file = files.iterator().next();
-            if (file.isLocked()) {
-                toShow.add(R.id.menu_group_lock_info);
-            } else {
-                toHide.add(R.id.menu_group_lock_info);
-            }
-        }
-    }
-
-    private void filterEncrypt(List<Integer> toShow, List<Integer> toHide, boolean endToEndEncryptionEnabled) {
+    private void filterEncrypt(List<Integer> toHide, boolean endToEndEncryptionEnabled) {
         if (files.isEmpty() || !isSingleSelection() || isSingleFile() || isEncryptedFolder() || isGroupFolder()
             || !endToEndEncryptionEnabled || !isEmptyFolder()) {
             toHide.add(R.id.action_encrypted);
-        } else {
-            toShow.add(R.id.action_encrypted);
         }
     }
 
-    private void filterUnsetEncrypted(List<Integer> toShow, List<Integer> toHide, boolean endToEndEncryptionEnabled) {
+    private void filterUnsetEncrypted(List<Integer> toHide, boolean endToEndEncryptionEnabled) {
         if (!endToEndEncryptionEnabled || files.isEmpty() || !isSingleSelection() || isSingleFile() || !isEncryptedFolder() || hasEncryptedParent()
             || !isEmptyFolder()) {
             toHide.add(R.id.action_unset_encrypted);
-        } else {
-            toShow.add(R.id.action_unset_encrypted);
         }
     }
 
-    private void filterSetPictureAs(List<Integer> toShow, List<Integer> toHide) {
-        if (isSingleImage() && !MimeTypeUtil.isSVG(files.iterator().next())) {
-            toShow.add(R.id.action_set_as_wallpaper);
-        } else {
+    private void filterSetPictureAs(List<Integer> toHide) {
+        if (!isSingleImage() || MimeTypeUtil.isSVG(files.iterator().next())) {
             toHide.add(R.id.action_set_as_wallpaper);
         }
     }
 
-    private void filterEdit(List<Integer> toShow,
-                            List<Integer> toHide,
-                            OCCapability capability
-    ) {
+    private void filterEdit(
+        List<Integer> toHide,
+        OCCapability capability
+                           ) {
         if (files.iterator().next().isEncrypted()) {
             toHide.add(R.id.action_edit);
             return;
@@ -354,44 +273,11 @@ public class FileMenuFilter {
 
         String mimeType = files.iterator().next().getMimeType();
 
-        if (isRichDocumentEditingSupported(capability, mimeType) || isEditorAvailable(context.getContentResolver(),
-                                                                                      user,
-                                                                                      mimeType)) {
-            toShow.add(R.id.action_edit);
-        } else {
+        if (!isRichDocumentEditingSupported(capability, mimeType) && !editorUtils.isEditorAvailable(user, mimeType)) {
             toHide.add(R.id.action_edit);
         }
     }
 
-    public static boolean isEditorAvailable(ContentResolver contentResolver, User user, String mimeType) {
-        return getEditor(contentResolver, user, mimeType) != null;
-    }
-
-    @Nullable
-    public static Editor getEditor(ContentResolver contentResolver, User user, String mimeType) {
-        String json = new ArbitraryDataProvider(contentResolver).getValue(user, ArbitraryDataProvider.DIRECT_EDITING);
-
-        if (json.isEmpty()) {
-            return null;
-        }
-
-        DirectEditing directEditing = new Gson().fromJson(json, DirectEditing.class);
-
-        for (Editor editor : directEditing.getEditors().values()) {
-            if (editor.getMimetypes().contains(mimeType)) {
-                return editor;
-            }
-        }
-
-        for (Editor editor : directEditing.getEditors().values()) {
-            if (editor.getOptionalMimetypes().contains(mimeType)) {
-                return editor;
-            }
-        }
-
-        return null;
-    }
-
     /**
      * This will be replaced by unified editor and can be removed once EOL of corresponding server version.
      */
@@ -403,31 +289,25 @@ public class FileMenuFilter {
             capability.getRichDocumentsDirectEditing().isTrue();
     }
 
-    private void filterSync(List<Integer> toShow, List<Integer> toHide, boolean synchronizing) {
+    private void filterSync(List<Integer> toHide, boolean synchronizing) {
         if (files.isEmpty() || (!anyFileDown() && !containsFolder()) || synchronizing) {
             toHide.add(R.id.action_sync_file);
-        } else {
-            toShow.add(R.id.action_sync_file);
         }
     }
 
-    private void filterCancelSync(List<Integer> toShow, List<Integer> toHide, boolean synchronizing) {
+    private void filterCancelSync(List<Integer> toHide, boolean synchronizing) {
         if (files.isEmpty() || !synchronizing) {
             toHide.add(R.id.action_cancel_sync);
-        } else {
-            toShow.add(R.id.action_cancel_sync);
         }
     }
 
-    private void filterOpenWith(Collection<Integer> toShow, Collection<Integer> toHide, boolean synchronizing) {
+    private void filterOpenWith(Collection<Integer> toHide, boolean synchronizing) {
         if (!isSingleFile() || !anyFileDown() || synchronizing) {
             toHide.add(R.id.action_open_file_with);
-        } else {
-            toShow.add(R.id.action_open_file_with);
         }
     }
 
-    private void filterDeselectAll(List<Integer> toShow, List<Integer> toHide, boolean inSingleFileFragment) {
+    private void filterDeselectAll(List<Integer> toHide, boolean inSingleFileFragment) {
         if (inSingleFileFragment) {
             // Always hide in single file fragments
             toHide.add(R.id.action_deselect_all_action_menu);
@@ -435,19 +315,15 @@ public class FileMenuFilter {
             // Show only if at least one item is selected.
             if (files.isEmpty() || overflowMenu) {
                 toHide.add(R.id.action_deselect_all_action_menu);
-            } else {
-                toShow.add(R.id.action_deselect_all_action_menu);
             }
         }
     }
 
-    private void filterSelectAll(List<Integer> toShow, List<Integer> toHide, boolean inSingleFileFragment) {
+    private void filterSelectAll(List<Integer> toHide, boolean inSingleFileFragment) {
         if (!inSingleFileFragment) {
             // Show only if at least one item isn't selected.
             if (files.size() >= numberOfAllFiles || overflowMenu) {
                 toHide.add(R.id.action_select_all_action_menu);
-            } else {
-                toShow.add(R.id.action_select_all_action_menu);
             }
         } else {
             // Always hide in single file fragments
@@ -455,60 +331,46 @@ public class FileMenuFilter {
         }
     }
 
-    private void filterRemove(List<Integer> toShow, List<Integer> toHide, boolean synchronizing) {
+    private void filterRemove(List<Integer> toHide, boolean synchronizing) {
         if (files.isEmpty() || synchronizing || containsLockedFile()) {
             toHide.add(R.id.action_remove_file);
-        } else {
-            toShow.add(R.id.action_remove_file);
         }
     }
 
-    private void filterMove(List<Integer> toShow, List<Integer> toHide, boolean synchronizing) {
+    private void filterMove(List<Integer> toHide, boolean synchronizing) {
         if (files.isEmpty() || synchronizing || containsEncryptedFile() || containsEncryptedFolder() || containsLockedFile()) {
             toHide.add(R.id.action_move);
-        } else {
-            toShow.add(R.id.action_move);
         }
     }
 
-    private void filterCopy(List<Integer> toShow, List<Integer> toHide, boolean synchronizing) {
+    private void filterCopy(List<Integer> toHide, boolean synchronizing) {
         if (files.isEmpty() || synchronizing || containsEncryptedFile() || containsEncryptedFolder()) {
             toHide.add(R.id.action_copy);
-        } else {
-            toShow.add(R.id.action_copy);
         }
     }
 
 
-    private void filterRename(Collection<Integer> toShow, Collection<Integer> toHide, boolean synchronizing) {
+    private void filterRename(Collection<Integer> toHide, boolean synchronizing) {
         if (!isSingleSelection() || synchronizing || containsEncryptedFile() || containsEncryptedFolder() || containsLockedFile()) {
             toHide.add(R.id.action_rename_file);
-        } else {
-            toShow.add(R.id.action_rename_file);
         }
     }
 
-    private void filterDownload(List<Integer> toShow, List<Integer> toHide, boolean synchronizing) {
+    private void filterDownload(List<Integer> toHide, boolean synchronizing) {
         if (files.isEmpty() || containsFolder() || anyFileDown() || synchronizing) {
             toHide.add(R.id.action_download_file);
-        } else {
-            toShow.add(R.id.action_download_file);
         }
     }
 
-    private void filterExport(List<Integer> toShow, List<Integer> toHide) {
+    private void filterExport(List<Integer> toHide) {
         if (files.isEmpty() || containsFolder()) {
             toHide.add(R.id.action_export_file);
-        } else {
-            toShow.add(R.id.action_export_file);
         }
     }
 
-    private void filterStream(List<Integer> toShow, List<Integer> toHide) {
+    private void filterStream(List<Integer> toHide) {
         if (files.isEmpty() || !isSingleFile() || !isSingleMedia()) {
             toHide.add(R.id.action_stream_media);
-        } else {
-            toShow.add(R.id.action_stream_media);
         }
     }
 
@@ -594,8 +456,7 @@ public class FileMenuFilter {
         if (isSingleSelection()) {
             OCFile file = files.iterator().next();
 
-            boolean noChildren = componentsGetter
-                .getStorageManager()
+            boolean noChildren = storageManager
                 .getFolderContent(file, false).size() == EMPTY_FILE_LENGTH;
 
             return file.isFolder() && file.getFileLength() == EMPTY_FILE_LENGTH && noChildren;
@@ -610,7 +471,7 @@ public class FileMenuFilter {
 
     private boolean hasEncryptedParent() {
         OCFile folder = files.iterator().next();
-        OCFile parent = componentsGetter.getStorageManager().getFileById(folder.getParentId());
+        OCFile parent = storageManager.getFileById(folder.getParentId());
 
         return parent != null && parent.isEncrypted();
     }

+ 5 - 1
app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java

@@ -45,6 +45,7 @@ import com.nextcloud.client.account.User;
 import com.nextcloud.client.account.UserAccountManager;
 import com.nextcloud.client.jobs.BackgroundJobManager;
 import com.nextcloud.client.network.ConnectivityService;
+import com.nextcloud.utils.EditorUtils;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
 import com.owncloud.android.authentication.AuthenticatorActivity;
@@ -174,6 +175,9 @@ public abstract class FileActivity extends DrawerActivity
     @Inject
     BackgroundJobManager backgroundJobManager;
 
+    @Inject
+    EditorUtils editorUtils;
+
     @Override
     public void showFiles(boolean onDeviceOnly) {
         // must be specialized in subclasses
@@ -196,7 +200,7 @@ public abstract class FileActivity extends DrawerActivity
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         mHandler = new Handler();
-        mFileOperationsHelper = new FileOperationsHelper(this, getUserAccountManager(), connectivityService);
+        mFileOperationsHelper = new FileOperationsHelper(this, getUserAccountManager(), connectivityService, editorUtils);
 
         if (savedInstanceState != null) {
             mFile = savedInstanceState.getParcelable(FileActivity.EXTRA_FILE);

+ 4 - 4
app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java

@@ -2110,7 +2110,7 @@ public class FileDisplayActivity extends FileActivity
         } else {
             FileOperationsHelper fileOperationsHelper = new FileOperationsHelper(this,
                                                                                  getUserAccountManager(),
-                                                                                 connectivityService);
+                                                                                 connectivityService, editorUtils);
             fileOperationsHelper.startSyncForFileAndIntent(file, showDetailsIntent);
         }
     }
@@ -2131,7 +2131,7 @@ public class FileDisplayActivity extends FileActivity
         } else {
             FileOperationsHelper fileOperationsHelper = new FileOperationsHelper(this,
                                                                                  getUserAccountManager(),
-                                                                                 connectivityService);
+                                                                                 connectivityService, editorUtils);
             fileOperationsHelper.startSyncForFileAndIntent(file, showDetailsIntent);
         }
     }
@@ -2163,7 +2163,7 @@ public class FileDisplayActivity extends FileActivity
             previewIntent.putExtra(PreviewVideoActivity.EXTRA_AUTOPLAY, autoplay);
             FileOperationsHelper fileOperationsHelper = new FileOperationsHelper(this,
                                                                                  getUserAccountManager(),
-                                                                                 connectivityService);
+                                                                                 connectivityService, editorUtils);
             fileOperationsHelper.startSyncForFileAndIntent(file, previewIntent);
         }
     }
@@ -2197,7 +2197,7 @@ public class FileDisplayActivity extends FileActivity
             previewIntent.putExtra(TEXT_PREVIEW, true);
             FileOperationsHelper fileOperationsHelper = new FileOperationsHelper(this,
                                                                                  getUserAccountManager(),
-                                                                                 connectivityService);
+                                                                                 connectivityService, editorUtils);
             fileOperationsHelper.startSyncForFileAndIntent(file, previewIntent);
         }
     }

+ 6 - 3
app/src/main/java/com/owncloud/android/ui/activity/TextEditorWebView.kt

@@ -29,8 +29,8 @@ import androidx.webkit.WebViewFeature
 import com.nextcloud.android.common.ui.util.PlatformThemeUtil
 import com.nextcloud.client.appinfo.AppInfo
 import com.nextcloud.client.device.DeviceInfo
+import com.nextcloud.utils.EditorUtils
 import com.owncloud.android.R
-import com.owncloud.android.files.FileMenuFilter
 import com.owncloud.android.ui.asynctasks.TextEditorLoadUrlTask
 import com.owncloud.android.utils.theme.ThemeUtils
 import javax.inject.Inject
@@ -45,6 +45,9 @@ class TextEditorWebView : EditorWebView() {
     @Inject
     lateinit var themeUtils: ThemeUtils
 
+    @Inject
+    lateinit var editorUtils: EditorUtils
+
     @SuppressLint("AddJavascriptInterface") // suppress warning as webview is only used > Lollipop
     override fun postOnCreate() {
         super.postOnCreate()
@@ -54,7 +57,7 @@ class TextEditorWebView : EditorWebView() {
             finish()
         }
 
-        val editor = FileMenuFilter.getEditor(contentResolver, user.get(), file.mimeType)
+        val editor = editorUtils.getEditor(user.get(), file.mimeType)
 
         if (editor != null && editor.id == "onlyoffice") {
             getWebView().settings.userAgentString = generateOnlyOfficeUserAgent()
@@ -79,7 +82,7 @@ class TextEditorWebView : EditorWebView() {
 
     override fun loadUrl(url: String?) {
         if (url.isNullOrEmpty()) {
-            TextEditorLoadUrlTask(this, user.get(), file).execute()
+            TextEditorLoadUrlTask(this, user.get(), file, editorUtils).execute()
         }
     }
 

+ 3 - 3
app/src/main/java/com/owncloud/android/ui/adapter/OCFileListItemViewHolder.kt

@@ -47,14 +47,14 @@ internal class OCFileListItemViewHolder(private var binding: ListItemBinding) :
     override val fileName: TextView
         get() = binding.Filename
     override val thumbnail: ImageView
-        get() = binding.thumbnail
+        get() = binding.thumbnailLayout.thumbnail
 
     override fun showVideoOverlay() {
-        binding.videoOverlay.visibility = View.VISIBLE
+        binding.thumbnailLayout.videoOverlay.visibility = View.VISIBLE
     }
 
     override val shimmerThumbnail: LoaderImageView
-        get() = binding.thumbnailShimmer
+        get() = binding.thumbnailLayout.thumbnailShimmer
     override val favorite: ImageView
         get() = binding.favoriteAction
     override val localFileIndicator: ImageView

+ 5 - 4
app/src/main/java/com/owncloud/android/ui/asynctasks/TextEditorLoadUrlTask.java

@@ -20,13 +20,12 @@
  */
 package com.owncloud.android.ui.asynctasks;
 
-import android.accounts.Account;
 import android.os.AsyncTask;
 
 import com.nextcloud.android.lib.resources.directediting.DirectEditingOpenFileRemoteOperation;
 import com.nextcloud.client.account.User;
+import com.nextcloud.utils.EditorUtils;
 import com.owncloud.android.datamodel.OCFile;
-import com.owncloud.android.files.FileMenuFilter;
 import com.owncloud.android.lib.common.Editor;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.ui.activity.EditorWebView;
@@ -35,14 +34,16 @@ import java.lang.ref.WeakReference;
 
 public class TextEditorLoadUrlTask extends AsyncTask<Void, Void, String> {
 
+    private final EditorUtils editorUtils;
     private WeakReference<EditorWebView> editorWebViewWeakReference;
     private OCFile file;
     private User user;
 
-    public TextEditorLoadUrlTask(EditorWebView editorWebView, User user, OCFile file) {
+    public TextEditorLoadUrlTask(EditorWebView editorWebView, User user, OCFile file, EditorUtils editorUtils) {
         this.user = user;
         this.editorWebViewWeakReference = new WeakReference<>(editorWebView);
         this.file = file;
+        this.editorUtils = editorUtils;
     }
 
     @Override
@@ -53,7 +54,7 @@ public class TextEditorLoadUrlTask extends AsyncTask<Void, Void, String> {
             return "";
         }
 
-        Editor editor = FileMenuFilter.getEditor(editorWebView.getContentResolver(), user, file.getMimeType());
+        Editor editor = editorUtils.getEditor(user, file.getMimeType());
 
         if (editor == null) {
             return "";

+ 32 - 47
app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java

@@ -32,11 +32,9 @@ import android.graphics.Bitmap;
 import android.os.Bundle;
 import android.view.LayoutInflater;
 import android.view.Menu;
-import android.view.MenuItem;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.view.ViewGroup;
-import android.widget.PopupMenu;
 import android.widget.ProgressBar;
 
 import com.google.android.material.floatingactionbutton.FloatingActionButton;
@@ -48,13 +46,14 @@ import com.nextcloud.client.jobs.BackgroundJobManager;
 import com.nextcloud.client.network.ClientFactory;
 import com.nextcloud.client.network.ConnectivityService;
 import com.nextcloud.client.preferences.AppPreferences;
+import com.nextcloud.ui.fileactions.FileActionsBottomSheet;
+import com.nextcloud.utils.MenuUtils;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
 import com.owncloud.android.databinding.FileDetailsFragmentBinding;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.datamodel.ThumbnailsCacheManager;
-import com.owncloud.android.files.FileMenuFilter;
 import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder;
 import com.owncloud.android.files.services.FileUploader.FileUploaderBinder;
 import com.owncloud.android.lib.common.OwnCloudClient;
@@ -80,12 +79,16 @@ import org.greenrobot.eventbus.ThreadMode;
 
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
 
 import javax.inject.Inject;
 
+import androidx.annotation.IdRes;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.core.content.res.ResourcesCompat;
+import androidx.fragment.app.FragmentManager;
 
 /**
  * This Fragment is used to display the details about a file.
@@ -240,13 +243,29 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
         }
     }
 
-    private void onOverflowIconClicked(View view) {
-        PopupMenu popup = new PopupMenu(getActivity(), view);
-        popup.inflate(R.menu.fragment_file_detail);
-        prepareOptionsMenu(popup.getMenu());
-
-        popup.setOnMenuItemClickListener(this::optionsItemSelected);
-        popup.show();
+    private void onOverflowIconClicked() {
+        final OCFile file = getFile();
+        final List<Integer> additionalFilter = new ArrayList<>(
+            Arrays.asList(
+                R.id.action_lock_file,
+                R.id.action_unlock_file,
+                R.id.action_edit,
+                R.id.action_favorite,
+                R.id.action_unset_favorite,
+                R.id.action_see_details,
+                R.id.action_move,
+                R.id.action_copy,
+                R.id.action_stream_media,
+                R.id.action_send_share_file,
+                R.id.action_select_all_action_menu));
+        if (getFile().isFolder()) {
+            additionalFilter.add(R.id.action_send_file);
+            additionalFilter.add(R.id.action_sync_file);
+        }
+        final FragmentManager fragmentManager = getChildFragmentManager();
+        FileActionsBottomSheet.newInstance(file, true, additionalFilter)
+            .setResultListener(fragmentManager, this, this::optionsItemSelected)
+            .show(fragmentManager, "actions");
     }
 
     private void setupViewPager() {
@@ -365,52 +384,24 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
     public void onPrepareOptionsMenu(@NonNull Menu menu) {
         super.onPrepareOptionsMenu(menu);
 
-        FileMenuFilter.hideAll(menu);
-    }
-
-    private void prepareOptionsMenu(Menu menu) {
-        if (containerActivity.getStorageManager() != null) {
-            User currentUser = accountManager.getUser();
-            FileMenuFilter mf = new FileMenuFilter(
-                getFile(),
-                containerActivity,
-                getActivity(),
-                false,
-                currentUser
-            );
-
-            mf.filter(menu, true);
-        }
-
-        if (getFile().isFolder()) {
-            FileMenuFilter.hideMenuItems(menu.findItem(R.id.action_send_file));
-            FileMenuFilter.hideMenuItems(menu.findItem(R.id.action_sync_file));
-        }
+        MenuUtils.hideAll(menu);
     }
 
-    private boolean optionsItemSelected(MenuItem item) {
-        int itemId = item.getItemId();
-
+    private void optionsItemSelected(@IdRes final int itemId) {
         if (itemId == R.id.action_send_file) {
             containerActivity.getFileOperationsHelper().sendShareFile(getFile(), true);
-            return true;
         } else if (itemId == R.id.action_open_file_with) {
             containerActivity.getFileOperationsHelper().openFile(getFile());
-            return true;
         } else if (itemId == R.id.action_remove_file) {
             RemoveFilesDialogFragment dialog = RemoveFilesDialogFragment.newInstance(getFile());
             dialog.show(getFragmentManager(), FTAG_CONFIRMATION);
-            return true;
         } else if (itemId == R.id.action_rename_file) {
             RenameFileDialogFragment dialog = RenameFileDialogFragment.newInstance(getFile(), parentFolder);
             dialog.show(getFragmentManager(), FTAG_RENAME_FILE);
-            return true;
         } else if (itemId == R.id.action_cancel_sync) {
             ((FileDisplayActivity) containerActivity).cancelTransference(getFile());
-            return true;
         } else if (itemId == R.id.action_download_file || itemId == R.id.action_sync_file) {
             containerActivity.getFileOperationsHelper().syncFile(getFile());
-            return true;
         } else if (itemId == R.id.action_export_file) {
             ArrayList<OCFile> list = new ArrayList<>();
             list.add(getFile());
@@ -418,17 +409,11 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
                                                                     getContext(),
                                                                     getView(),
                                                                     backgroundJobManager);
-            return true;
         } else if (itemId == R.id.action_set_as_wallpaper) {
             containerActivity.getFileOperationsHelper().setPictureAs(getFile(), getView());
-            return true;
         } else if (itemId == R.id.action_encrypted) {// TODO implement or remove
-            return true;
         } else if (itemId == R.id.action_unset_encrypted) {// TODO implement or remove
-            return true;
         }
-
-        return super.onOptionsItemSelected(item);
     }
 
     @Override
@@ -441,7 +426,7 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
             containerActivity.getFileOperationsHelper().toggleFavoriteFile(getFile(), !getFile().isFavorite());
             setFavoriteIconStatus(!getFile().isFavorite());
         } else if (id == R.id.overflow_menu) {
-            onOverflowIconClicked(v);
+            onOverflowIconClicked();
         } else if (id == R.id.last_modification_timestamp) {
             boolean showDetailedTimestamp = !preferences.isShowDetailedTimestampEnabled();
             preferences.setShowDetailedTimestampEnabled(showDetailedTimestamp);

+ 7 - 5
app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.java

@@ -28,12 +28,12 @@ import com.google.gson.Gson;
 import com.nextcloud.client.account.User;
 import com.nextcloud.client.device.DeviceInfo;
 import com.nextcloud.client.di.Injectable;
+import com.nextcloud.utils.EditorUtils;
 import com.owncloud.android.R;
 import com.owncloud.android.databinding.FileListActionsBottomSheetCreatorBinding;
 import com.owncloud.android.databinding.FileListActionsBottomSheetFragmentBinding;
 import com.owncloud.android.datamodel.ArbitraryDataProvider;
 import com.owncloud.android.datamodel.OCFile;
-import com.owncloud.android.files.FileMenuFilter;
 import com.owncloud.android.lib.common.Creator;
 import com.owncloud.android.lib.common.DirectEditing;
 import com.owncloud.android.lib.resources.status.OCCapability;
@@ -56,6 +56,7 @@ public class OCFileListBottomSheetDialog extends BottomSheetDialog implements In
     private final OCFile file;
     private final ThemeUtils themeUtils;
     private final ViewThemeUtils viewThemeUtils;
+    private final EditorUtils editorUtils;
 
 
     public OCFileListBottomSheetDialog(FileActivity fileActivity,
@@ -64,7 +65,8 @@ public class OCFileListBottomSheetDialog extends BottomSheetDialog implements In
                                        User user,
                                        OCFile file,
                                        ThemeUtils themeUtils,
-                                       ViewThemeUtils viewThemeUtils) {
+                                       ViewThemeUtils viewThemeUtils,
+                                       EditorUtils editorUtils) {
         super(fileActivity);
         this.actions = actions;
         this.fileActivity = fileActivity;
@@ -73,6 +75,7 @@ public class OCFileListBottomSheetDialog extends BottomSheetDialog implements In
         this.file = file;
         this.themeUtils = themeUtils;
         this.viewThemeUtils = viewThemeUtils;
+        this.editorUtils = editorUtils;
     }
 
     @Override
@@ -142,9 +145,8 @@ public class OCFileListBottomSheetDialog extends BottomSheetDialog implements In
         }
 
         // create rich workspace
-        if (FileMenuFilter.isEditorAvailable(getContext().getContentResolver(),
-                                             user,
-                                             MimeTypeUtil.MIMETYPE_TEXT_MARKDOWN) &&
+        if (editorUtils.isEditorAvailable(user,
+                                          MimeTypeUtil.MIMETYPE_TEXT_MARKDOWN) &&
             file != null && !file.isEncrypted()) {
             // richWorkspace
             // == "": no info set -> show button

+ 6 - 1
app/src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialogFragment.kt

@@ -27,6 +27,7 @@ import androidx.fragment.app.DialogFragment
 import com.nextcloud.client.account.User
 import com.nextcloud.client.device.DeviceInfo
 import com.nextcloud.client.di.Injectable
+import com.nextcloud.utils.EditorUtils
 import com.owncloud.android.datamodel.OCFile
 import com.owncloud.android.ui.activity.FileActivity
 import com.owncloud.android.utils.theme.ThemeUtils
@@ -47,6 +48,9 @@ class OCFileListBottomSheetDialogFragment(
     @Inject
     lateinit var viewThemeUtils: ViewThemeUtils
 
+    @Inject
+    lateinit var editorUtils: EditorUtils
+
     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
         return OCFileListBottomSheetDialog(
             fileActivity,
@@ -55,7 +59,8 @@ class OCFileListBottomSheetDialogFragment(
             user,
             file,
             themeUtils,
-            viewThemeUtils
+            viewThemeUtils,
+            editorUtils
         )
     }
 }

+ 34 - 46
app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java

@@ -45,8 +45,6 @@ import android.widget.Toast;
 import com.google.android.material.behavior.HideBottomViewOnScrollBehavior;
 import com.google.android.material.floatingactionbutton.FloatingActionButton;
 import com.google.android.material.snackbar.Snackbar;
-import com.nextcloud.android.files.FileLockingMenuCustomization;
-import com.nextcloud.android.files.ThemedPopupMenu;
 import com.nextcloud.android.lib.resources.files.ToggleFileLockRemoteOperation;
 import com.nextcloud.android.lib.richWorkspace.RichWorkspaceDirectEditingRemoteOperation;
 import com.nextcloud.client.account.User;
@@ -58,6 +56,8 @@ import com.nextcloud.client.network.ClientFactory;
 import com.nextcloud.client.preferences.AppPreferences;
 import com.nextcloud.client.utils.Throttler;
 import com.nextcloud.common.NextcloudClient;
+import com.nextcloud.ui.fileactions.FileActionsBottomSheet;
+import com.nextcloud.utils.EditorUtils;
 import com.nextcloud.utils.view.FastScrollUtils;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
@@ -65,7 +65,6 @@ import com.owncloud.android.datamodel.ArbitraryDataProvider;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.datamodel.VirtualFolderType;
-import com.owncloud.android.files.FileMenuFilter;
 import com.owncloud.android.lib.common.Creator;
 import com.owncloud.android.lib.common.OwnCloudClient;
 import com.owncloud.android.lib.common.operations.RemoteOperation;
@@ -118,21 +117,23 @@ import org.greenrobot.eventbus.ThreadMode;
 import java.io.File;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
 import javax.inject.Inject;
 
+import androidx.annotation.IdRes;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.StringRes;
 import androidx.appcompat.app.ActionBar;
 import androidx.coordinatorlayout.widget.CoordinatorLayout;
 import androidx.core.content.ContextCompat;
+import androidx.core.content.res.ResourcesCompat;
 import androidx.drawerlayout.widget.DrawerLayout;
 import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
 import androidx.recyclerview.widget.GridLayoutManager;
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
@@ -196,6 +197,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
     @Inject BackgroundJobManager backgroundJobManager;
     @Inject ViewThemeUtils viewThemeUtils;
     @Inject FastScrollUtils fastScrollUtils;
+    @Inject EditorUtils editorUtils;
 
     protected FileFragment.ContainerActivity mContainerActivity;
 
@@ -571,23 +573,20 @@ public class OCFileListFragment extends ExtendedListFragment implements
 
     @Override
     public void onOverflowIconClicked(OCFile file, View view) {
-        throttler.run("overflowClick", () -> {
-            final ThemedPopupMenu popup = new ThemedPopupMenu(requireContext(), view);
-            popup.inflate(R.menu.item_file);
-            FileMenuFilter mf = new FileMenuFilter(mAdapter.getFiles().size(),
-                                                   Collections.singleton(file),
-                                                   mContainerActivity, getActivity(),
-                                                   true,
-                                                   accountManager.getUser());
-            mf.filter(popup.getMenu(), true);
-            new FileLockingMenuCustomization(requireContext()).customizeMenu(popup.getMenu(), file);
-            popup.setOnMenuItemClickListener(item -> {
-                Set<OCFile> checkedFiles = new HashSet<>();
-                checkedFiles.add(file);
-                return onFileActionChosen(item, checkedFiles);
-            });
+        final Set<OCFile> checkedFiles = new HashSet<>();
+        checkedFiles.add(file);
+        openActionsMenu(1, checkedFiles, true);
+    }
 
-            popup.show();
+    public void openActionsMenu(final int filesCount, final Set<OCFile> checkedFiles, final boolean isOverflow) {
+        throttler.run("overflowClick", () -> {
+            final FragmentManager childFragmentManager = getChildFragmentManager();
+            FileActionsBottomSheet.newInstance(filesCount, checkedFiles, isOverflow)
+                .setResultListener(childFragmentManager, this, (id) -> {
+                    onFileActionChosen(id, checkedFiles);
+                })
+                .show(childFragmentManager, "actions");
+            ;
         });
     }
 
@@ -710,8 +709,11 @@ public class OCFileListFragment extends ExtendedListFragment implements
             // Determine if actionMode is "new" or not (already affected by item-selection)
             mIsActionModeNew = true;
 
+            // fake menu to be able to use bottom sheet instead
             MenuInflater inflater = getActivity().getMenuInflater();
-            inflater.inflate(R.menu.item_file, menu);
+            inflater.inflate(R.menu.custom_menu_placeholder, menu);
+            final MenuItem item = menu.findItem(R.id.custom_menu_placeholder_item);
+            item.setIcon(viewThemeUtils.platform.colorDrawable(item.getIcon(), ContextCompat.getColor(requireContext(), R.color.white)));
             mode.invalidate();
 
             //set actionMode color
@@ -726,6 +728,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
             return true;
         }
 
+
         /**
          * Updates available action in menu depending on current selection.
          */
@@ -735,23 +738,10 @@ public class OCFileListFragment extends ExtendedListFragment implements
             final int checkedCount = checkedFiles.size();
             String title = getResources().getQuantityString(R.plurals.items_selected_count, checkedCount, checkedCount);
             mode.setTitle(title);
-            FileMenuFilter mf = new FileMenuFilter(
-                getCommonAdapter().getFilesCount(),
-                checkedFiles,
-                mContainerActivity,
-                getActivity(),
-                false,
-                accountManager.getUser()
-            );
-
-            mf.filter(menu, false);
 
             // Determine if we need to finish the action mode because there are no items selected
             if (checkedCount == 0 && !mIsActionModeNew) {
                 exitSelectionMode();
-            } else if (checkedCount == 1) {
-                // customize for locking if file is locked
-                new FileLockingMenuCustomization(requireContext()).customizeMenu(menu, checkedFiles.iterator().next());
             }
 
             return true;
@@ -762,8 +752,11 @@ public class OCFileListFragment extends ExtendedListFragment implements
          */
         @Override
         public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
-            Set<OCFile> checkedFiles = getCommonAdapter().getCheckedItems();
-            return onFileActionChosen(item, checkedFiles);
+            final Set<OCFile> checkedFiles = getCommonAdapter().getCheckedItems();
+            if (item.getItemId() == R.id.custom_menu_placeholder_item) {
+                openActionsMenu(getCommonAdapter().getFilesCount(), checkedFiles, false);
+            }
+            return true;
         }
 
         /**
@@ -1051,9 +1044,8 @@ public class OCFileListFragment extends ExtendedListFragment implements
                             // stream media preview on >= NC14
                             setFabVisible(false);
                             ((FileDisplayActivity) mContainerActivity).startMediaPreview(file, 0, true, true, true);
-                        } else if (FileMenuFilter.isEditorAvailable(requireContext().getContentResolver(),
-                                                                    accountManager.getUser(),
-                                                                    file.getMimeType()) &&
+                        } else if (editorUtils.isEditorAvailable(accountManager.getUser(),
+                                                                 file.getMimeType()) &&
                             !file.isEncrypted()) {
                             mContainerActivity.getFileOperationsHelper().openFileWithTextEditor(file, getContext());
                         } else if (capability.getRichDocumentsMimeTypeList().contains(file.getMimeType()) &&
@@ -1099,11 +1091,10 @@ public class OCFileListFragment extends ExtendedListFragment implements
     /**
      * Start the appropriate action(s) on the currently selected files given menu selected by the user.
      *
-     * @param item       MenuItem selected by the user
      * @param checkedFiles List of files selected by the user on which the action should be performed
      * @return 'true' if the menu selection started any action, 'false' otherwise.
      */
-    public boolean onFileActionChosen(MenuItem item, Set<OCFile> checkedFiles) {
+    public boolean onFileActionChosen(@IdRes final int itemId, Set<OCFile> checkedFiles) {
         if (checkedFiles.isEmpty()) {
             return false;
         }
@@ -1111,7 +1102,6 @@ public class OCFileListFragment extends ExtendedListFragment implements
         if (checkedFiles.size() == SINGLE_SELECTION) {
             /// action only possible on a single file
             OCFile singleFile = checkedFiles.iterator().next();
-            int itemId = item.getItemId();
 
             if (itemId == R.id.action_send_share_file) {
                 mContainerActivity.getFileOperationsHelper().sendShareFile(singleFile);
@@ -1124,9 +1114,8 @@ public class OCFileListFragment extends ExtendedListFragment implements
                 return true;
             } else if (itemId == R.id.action_edit) {
                 // should not be necessary, as menu item is filtered, but better play safe
-                if (FileMenuFilter.isEditorAvailable(requireContext().getContentResolver(),
-                                                     accountManager.getUser(),
-                                                     singleFile.getMimeType())) {
+                if (editorUtils.isEditorAvailable(accountManager.getUser(),
+                                                  singleFile.getMimeType())) {
                     mContainerActivity.getFileOperationsHelper().openFileWithTextEditor(singleFile, getContext());
                 } else {
                     mContainerActivity.getFileOperationsHelper().openFileAsRichDocument(singleFile, getContext());
@@ -1162,7 +1151,6 @@ public class OCFileListFragment extends ExtendedListFragment implements
         }
 
         /// actions possible on a batch of files
-        int itemId = item.getItemId();
         if (itemId == R.id.action_remove_file) {
             RemoveFilesDialogFragment dialog =
                 RemoveFilesDialogFragment.newInstance(new ArrayList<>(checkedFiles), mActiveActionMode);

+ 5 - 5
app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java

@@ -51,12 +51,12 @@ import com.nextcloud.client.account.User;
 import com.nextcloud.client.jobs.BackgroundJobManager;
 import com.nextcloud.client.network.ConnectivityService;
 import com.nextcloud.java.util.Optional;
+import com.nextcloud.utils.EditorUtils;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
 import com.owncloud.android.datamodel.ArbitraryDataProvider;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
-import com.owncloud.android.files.FileMenuFilter;
 import com.owncloud.android.files.StreamMediaFileOperation;
 import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder;
 import com.owncloud.android.files.services.FileUploader.FileUploaderBinder;
@@ -130,16 +130,18 @@ public class FileOperationsHelper {
     private final FileActivity fileActivity;
     private final CurrentAccountProvider currentAccount;
     private final ConnectivityService connectivityService;
+    private final EditorUtils editorUtils;
 
     /// Identifier of operation in progress which result shouldn't be lost
     private long mWaitingForOpId = Long.MAX_VALUE;
 
     public FileOperationsHelper(FileActivity fileActivity,
                                 CurrentAccountProvider currentAccount,
-                                ConnectivityService connectivityService) {
+                                ConnectivityService connectivityService, EditorUtils editorUtils) {
         this.fileActivity = fileActivity;
         this.currentAccount = currentAccount;
         this.connectivityService = connectivityService;
+        this.editorUtils = editorUtils;
     }
 
     @Nullable
@@ -304,9 +306,7 @@ public class FileOperationsHelper {
             if (launchables.isEmpty()) {
                 Optional<User> optionalUser = fileActivity.getUser();
 
-                if (optionalUser.isPresent() && FileMenuFilter.isEditorAvailable(fileActivity.getContentResolver(),
-                                                                                 optionalUser.get(),
-                                                                                 file.getMimeType())) {
+                if (optionalUser.isPresent() && editorUtils.isEditorAvailable(optionalUser.get(), file.getMimeType())) {
                     openFileWithTextEditor(file, fileActivity);
                 } else {
                     Account account = fileActivity.getAccount();

+ 43 - 66
app/src/main/java/com/owncloud/android/ui/preview/PreviewImageFragment.java

@@ -23,10 +23,8 @@ package com.owncloud.android.ui.preview;
 
 import android.app.Activity;
 import android.content.Context;
-import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
-import android.graphics.Color;
 import android.graphics.Point;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
@@ -35,8 +33,6 @@ import android.graphics.drawable.PictureDrawable;
 import android.os.AsyncTask;
 import android.os.Bundle;
 import android.os.Process;
-import android.text.SpannableString;
-import android.text.style.ForegroundColorSpan;
 import android.util.DisplayMetrics;
 import android.view.LayoutInflater;
 import android.view.Menu;
@@ -57,12 +53,12 @@ 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.ui.fileactions.FileActionsBottomSheet;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
 import com.owncloud.android.databinding.PreviewImageFragmentBinding;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.datamodel.ThumbnailsCacheManager;
-import com.owncloud.android.files.FileMenuFilter;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.ui.dialog.ConfirmationDialogFragment;
 import com.owncloud.android.ui.dialog.RemoveFilesDialogFragment;
@@ -71,19 +67,24 @@ import com.owncloud.android.utils.BitmapUtils;
 import com.owncloud.android.utils.DisplayUtils;
 import com.owncloud.android.utils.MimeType;
 import com.owncloud.android.utils.MimeTypeUtil;
+import com.owncloud.android.utils.theme.ViewThemeUtils;
 
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
 
 import javax.annotation.Nullable;
 import javax.inject.Inject;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.StringRes;
+import androidx.core.content.ContextCompat;
 import androidx.core.content.res.ResourcesCompat;
+import androidx.fragment.app.FragmentManager;
 import androidx.fragment.app.FragmentStatePagerAdapter;
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import pl.droidsonroids.gif.GifDrawable;
@@ -125,6 +126,8 @@ public class PreviewImageFragment extends FileFragment implements Injectable {
     @Inject ConnectivityService connectivityService;
     @Inject UserAccountManager accountManager;
     @Inject BackgroundJobManager backgroundJobManager;
+    @Inject ViewThemeUtils viewThemeUtils;
+
     private PreviewImageFragmentBinding binding;
 
     /**
@@ -343,72 +346,54 @@ public class PreviewImageFragment extends FileFragment implements Injectable {
     @Override
     public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
         super.onCreateOptionsMenu(menu, inflater);
-        inflater.inflate(R.menu.item_file, menu);
-
-        int nightModeFlag = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
-
-        if (Configuration.UI_MODE_NIGHT_NO == nightModeFlag) {
-            for (int i = 0; i < menu.size(); i++) {
-                MenuItem menuItem = menu.getItem(i);
-
-                SpannableString spanString = new SpannableString(menuItem.getTitle().toString());
-                spanString.setSpan(new ForegroundColorSpan(Color.BLACK), 0, spanString.length(), 0);
-                menuItem.setTitle(spanString);
-            }
-        }
+        inflater.inflate(R.menu.custom_menu_placeholder, menu);
+        final MenuItem item = menu.findItem(R.id.custom_menu_placeholder_item);
+        item.setIcon(viewThemeUtils.platform.colorDrawable(item.getIcon(), ContextCompat.getColor(requireContext(), R.color.white)));
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
-    public void onPrepareOptionsMenu(@NonNull Menu menu) {
-        super.onPrepareOptionsMenu(menu);
-
-        if (containerActivity.getStorageManager() != null && getFile() != null) {
-            // Update the file
-            final OCFile updatedFile = containerActivity.getStorageManager().getFileById(getFile().getFileId());
-            setFile(updatedFile);
-
-            if (getFile() != null) {
-                User currentUser = accountManager.getUser();
-                FileMenuFilter mf = new FileMenuFilter(
-                    getFile(),
-                    containerActivity,
-                    getActivity(),
-                    false,
-                    currentUser
-                );
-
-                mf.filter(menu, true);
+    public boolean onOptionsItemSelected(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);
+    }
 
-        // additional restriction for this fragment
-        // TODO allow renaming in PreviewImageFragment
-        // TODO allow refresh file in PreviewImageFragment
-        FileMenuFilter.hideMenuItems(
-                menu.findItem(R.id.action_rename_file),
-                menu.findItem(R.id.action_sync_file),
-                menu.findItem(R.id.action_select_all),
-                menu.findItem(R.id.action_move),
-                menu.findItem(R.id.action_copy),
-                menu.findItem(R.id.action_favorite),
-                menu.findItem(R.id.action_unset_favorite)
-        );
-
+    private void showFileActions(OCFile file) {
+        final List<Integer> additionalFilter = new ArrayList<>(
+            Arrays.asList(
+                R.id.action_rename_file,
+                R.id.action_sync_file,
+                R.id.action_select_all,
+                R.id.action_move,
+                R.id.action_copy,
+                R.id.action_favorite,
+                R.id.action_unset_favorite
+                         ));
         if (getFile() != null && getFile().isSharedWithMe() && !getFile().canReshare()) {
-            FileMenuFilter.hideMenuItem(menu.findItem(R.id.action_send_share_file));
+            additionalFilter.add(R.id.action_send_share_file);
         }
+        final FragmentManager fragmentManager = getChildFragmentManager();
+        FileActionsBottomSheet.newInstance(file, false, additionalFilter)
+            .setResultListener(fragmentManager, this, this::onFileActionChosen)
+            .show(fragmentManager, "actions");
     }
 
-
     /**
      * {@inheritDoc}
      */
-    @Override
-    public boolean onOptionsItemSelected(MenuItem item) {
-        int itemId = item.getItemId();
+    public void onFileActionChosen(final int itemId) {
         if (itemId == R.id.action_send_share_file) {
             if (getFile().isSharedWithMe() && !getFile().canReshare()) {
                 Snackbar.make(requireView(),
@@ -419,23 +404,17 @@ public class PreviewImageFragment extends FileFragment implements Injectable {
             } else {
                 containerActivity.getFileOperationsHelper().sendShareFile(getFile());
             }
-            return true;
         } else if (itemId == R.id.action_open_file_with) {
             openFile();
-            return true;
         } else if (itemId == R.id.action_remove_file) {
             RemoveFilesDialogFragment dialog = RemoveFilesDialogFragment.newInstance(getFile());
             dialog.show(getFragmentManager(), ConfirmationDialogFragment.FTAG_CONFIRMATION);
-            return true;
         } else if (itemId == R.id.action_see_details) {
             seeDetails();
-            return true;
         } else if (itemId == R.id.action_download_file || itemId == R.id.action_sync_file) {
             containerActivity.getFileOperationsHelper().syncFile(getFile());
-            return true;
         } else if (itemId == R.id.action_set_as_wallpaper) {
             containerActivity.getFileOperationsHelper().setPictureAs(getFile(), getImageView());
-            return true;
         } else if (itemId == R.id.action_export_file) {
             ArrayList<OCFile> list = new ArrayList<>();
             list.add(getFile());
@@ -443,9 +422,7 @@ public class PreviewImageFragment extends FileFragment implements Injectable {
                                                                     getContext(),
                                                                     getView(),
                                                                     backgroundJobManager);
-            return true;
         }
-        return super.onOptionsItemSelected(item);
     }
 
     private void seeDetails() {

+ 39 - 76
app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.java

@@ -57,11 +57,11 @@ import com.nextcloud.client.media.NextcloudExoPlayer;
 import com.nextcloud.client.media.PlayerServiceConnection;
 import com.nextcloud.client.network.ClientFactory;
 import com.nextcloud.common.NextcloudClient;
+import com.nextcloud.ui.fileactions.FileActionsBottomSheet;
 import com.owncloud.android.R;
 import com.owncloud.android.databinding.FragmentPreviewMediaBinding;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.datamodel.ThumbnailsCacheManager;
-import com.owncloud.android.files.FileMenuFilter;
 import com.owncloud.android.files.StreamMediaFileOperation;
 import com.owncloud.android.lib.common.OwnCloudClient;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
@@ -76,6 +76,8 @@ import com.owncloud.android.utils.MimeTypeUtil;
 
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
 import java.util.concurrent.Executors;
 
 import javax.inject.Inject;
@@ -84,6 +86,7 @@ import androidx.annotation.NonNull;
 import androidx.annotation.StringRes;
 import androidx.appcompat.widget.AppCompatImageButton;
 import androidx.drawerlayout.widget.DrawerLayout;
+import androidx.fragment.app.FragmentManager;
 
 /**
  * This fragment shows a preview of a downloaded media file (audio or video).
@@ -368,98 +371,60 @@ public class PreviewMediaFragment extends FileFragment implements OnTouchListene
     public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
         super.onCreateOptionsMenu(menu, inflater);
         menu.removeItem(R.id.action_search);
-        inflater.inflate(R.menu.item_file, menu);
+        inflater.inflate(R.menu.custom_menu_placeholder, menu);
     }
 
     @Override
-    public void onPrepareOptionsMenu(@NonNull Menu menu) {
-        super.onPrepareOptionsMenu(menu);
-
-        if (containerActivity.getStorageManager() != null) {
-            User currentUser = accountManager.getUser();
-            FileMenuFilter mf = new FileMenuFilter(
-                getFile(),
-                containerActivity,
-                getActivity(),
-                false,
-                currentUser
-            );
-
-            mf.filter(menu, true);
-        }
-
-        // additional restriction for this fragment
-        // TODO allow renaming in PreviewImageFragment
-        MenuItem item = menu.findItem(R.id.action_rename_file);
-        if (item != null) {
-            item.setVisible(false);
-            item.setEnabled(false);
-        }
-
-        // additional restriction for this fragment
-        item = menu.findItem(R.id.action_select_all);
-        if (item != null) {
-            item.setVisible(false);
-            item.setEnabled(false);
-        }
-
-        // additional restriction for this fragment
-        item = menu.findItem(R.id.action_move);
-        if (item != null) {
-            item.setVisible(false);
-            item.setEnabled(false);
-        }
-
-        // additional restriction for this fragment
-        item = menu.findItem(R.id.action_copy);
-        if (item != null) {
-            item.setVisible(false);
-            item.setEnabled(false);
-        }
-
-        // additional restriction for this fragment
-        item = menu.findItem(R.id.action_favorite);
-        if (item != null) {
-            item.setVisible(false);
-            item.setEnabled(false);
-        }
-
-        // additional restriction for this fragment
-        item = menu.findItem(R.id.action_unset_favorite);
-        if (item != null) {
-            item.setVisible(false);
-            item.setEnabled(false);
+    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);
+    }
 
-        if (getFile().isSharedWithMe() && !getFile().canReshare()) {
-            // additional restriction for this fragment
-            item = menu.findItem(R.id.action_send_share_file);
-            if (item != null) {
-                item.setVisible(false);
-                item.setEnabled(false);
-            }
+    private void showFileActions(OCFile file) {
+        final List<Integer> additionalFilter = new ArrayList<>(
+            Arrays.asList(
+                R.id.action_rename_file,
+                R.id.action_sync_file,
+                R.id.action_select_all,
+                R.id.action_move,
+                R.id.action_copy,
+                R.id.action_favorite,
+                R.id.action_unset_favorite
+                         ));
+        if (getFile() != null && getFile().isSharedWithMe() && !getFile().canReshare()) {
+            additionalFilter.add(R.id.action_send_share_file);
         }
+        final FragmentManager fragmentManager = getChildFragmentManager();
+        FileActionsBottomSheet.newInstance(file, false, additionalFilter)
+            .setResultListener(fragmentManager, this, this::onFileActionChosen)
+            .show(fragmentManager, "actions");
     }
 
-    @Override
-    public boolean onOptionsItemSelected(MenuItem item) {
-        int itemId = item.getItemId();
+    public void onFileActionChosen(final int itemId) {
         if (itemId == R.id.action_send_share_file) {
             sendShareFile();
-            return true;
         } else if (itemId == R.id.action_open_file_with) {
             openFile();
-            return true;
         } else if (itemId == R.id.action_remove_file) {
             RemoveFilesDialogFragment dialog = RemoveFilesDialogFragment.newInstance(getFile());
             dialog.show(getFragmentManager(), ConfirmationDialogFragment.FTAG_CONFIRMATION);
-            return true;
         } else if (itemId == R.id.action_see_details) {
             seeDetails();
-            return true;
         } else if (itemId == R.id.action_sync_file) {
             containerActivity.getFileOperationsHelper().syncFile(getFile());
-            return true;
         } else if (itemId == R.id.action_stream_media) {
             containerActivity.getFileOperationsHelper().streamMediaFile(getFile());
         } else if (itemId == R.id.action_export_file) {
@@ -469,9 +434,7 @@ public class PreviewMediaFragment extends FileFragment implements OnTouchListene
                                                                     getContext(),
                                                                     getView(),
                                                                     backgroundJobManager);
-            return true;
         }
-        return super.onOptionsItemSelected(item);
     }
 
     /**

+ 39 - 46
app/src/main/java/com/owncloud/android/ui/preview/PreviewTextFileFragment.java

@@ -34,9 +34,9 @@ import android.widget.TextView;
 
 import com.nextcloud.client.account.User;
 import com.nextcloud.client.account.UserAccountManager;
+import com.nextcloud.ui.fileactions.FileActionsBottomSheet;
 import com.owncloud.android.R;
 import com.owncloud.android.datamodel.OCFile;
-import com.owncloud.android.files.FileMenuFilter;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.ui.dialog.ConfirmationDialogFragment;
 import com.owncloud.android.ui.dialog.RemoveFilesDialogFragment;
@@ -52,6 +52,8 @@ import java.io.IOException;
 import java.io.Reader;
 import java.io.StringWriter;
 import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Scanner;
@@ -61,6 +63,7 @@ import javax.inject.Inject;
 import androidx.annotation.NonNull;
 import androidx.appcompat.widget.SearchView;
 import androidx.core.view.MenuItemCompat;
+import androidx.fragment.app.FragmentManager;
 
 public class PreviewTextFileFragment extends PreviewTextFragment {
     private static final String EXTRA_FILE = "FILE";
@@ -256,7 +259,7 @@ public class PreviewTextFileFragment extends PreviewTextFragment {
     @Override
     public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
         super.onCreateOptionsMenu(menu, inflater);
-        inflater.inflate(R.menu.item_file, menu);
+        inflater.inflate(R.menu.custom_menu_placeholder, menu);
 
         MenuItem menuItem = menu.findItem(R.id.action_search);
         menuItem.setVisible(true);
@@ -271,74 +274,64 @@ public class PreviewTextFileFragment extends PreviewTextFragment {
         }
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
-    public void onPrepareOptionsMenu(@NonNull Menu menu) {
-        super.onPrepareOptionsMenu(menu);
-
-        if (containerActivity.getStorageManager() != null) {
-            User user = accountManager.getUser();
-            FileMenuFilter mf = new FileMenuFilter(
-                getFile(),
-                containerActivity,
-                getActivity(),
-                false,
-                user
-            );
-            mf.filter(menu, true);
+    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);
+    }
 
-        // additional restriction for this fragment
-        FileMenuFilter.hideMenuItems(
-            menu.findItem(R.id.action_rename_file),
-            menu.findItem(R.id.action_select_all),
-            menu.findItem(R.id.action_move),
-            menu.findItem(R.id.action_download_file),
-            menu.findItem(R.id.action_sync_file),
-            menu.findItem(R.id.action_favorite),
-            menu.findItem(R.id.action_unset_favorite)
-        );
-
-        if (getFile().isSharedWithMe() && !getFile().canReshare()) {
-            FileMenuFilter.hideMenuItem(menu.findItem(R.id.action_send_share_file));
+    private void showFileActions(OCFile file) {
+        final List<Integer> additionalFilter = new ArrayList<>(
+            Arrays.asList(
+                R.id.action_rename_file,
+                R.id.action_sync_file,
+                R.id.action_select_all,
+                R.id.action_move,
+                R.id.action_copy,
+                R.id.action_favorite,
+                R.id.action_unset_favorite
+                         ));
+        if (getFile() != null && getFile().isSharedWithMe() && !getFile().canReshare()) {
+            additionalFilter.add(R.id.action_send_share_file);
         }
+        final FragmentManager fragmentManager = getChildFragmentManager();
+        FileActionsBottomSheet.newInstance(file, false, additionalFilter)
+            .setResultListener(fragmentManager, this, this::onFileActionChosen)
+            .show(fragmentManager, "actions");
     }
 
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public boolean onOptionsItemSelected(MenuItem item) {
-        int itemId = item.getItemId();
-
+    private void onFileActionChosen(final int itemId) {
         if (itemId == R.id.action_send_share_file) {
             if (getFile().isSharedWithMe() && !getFile().canReshare()) {
                 DisplayUtils.showSnackMessage(getView(), R.string.resharing_is_not_allowed);
             } else {
                 containerActivity.getFileOperationsHelper().sendShareFile(getFile());
             }
-            return true;
         } else if (itemId == R.id.action_open_file_with) {
             openFile();
-            return true;
         } else if (itemId == R.id.action_remove_file) {
             RemoveFilesDialogFragment dialog = RemoveFilesDialogFragment.newInstance(getFile());
             dialog.show(getFragmentManager(), ConfirmationDialogFragment.FTAG_CONFIRMATION);
-            return true;
         } else if (itemId == R.id.action_see_details) {
             seeDetails();
-            return true;
         } else if (itemId == R.id.action_sync_file) {
             containerActivity.getFileOperationsHelper().syncFile(getFile());
-            return true;
         } else if (itemId == R.id.action_edit) {
             containerActivity.getFileOperationsHelper().openFileWithTextEditor(getFile(), getContext());
-            return true;
         }
-
-        return super.onOptionsItemSelected(item);
     }
 
     /**

+ 2 - 2
app/src/main/java/com/owncloud/android/ui/preview/pdf/PreviewPdfFragment.kt

@@ -33,10 +33,10 @@ import androidx.lifecycle.ViewModelProvider
 import com.google.android.material.snackbar.Snackbar
 import com.nextcloud.client.di.Injectable
 import com.nextcloud.client.di.ViewModelFactory
+import com.nextcloud.utils.MenuUtils
 import com.owncloud.android.R
 import com.owncloud.android.databinding.PreviewPdfFragmentBinding
 import com.owncloud.android.datamodel.OCFile
-import com.owncloud.android.files.FileMenuFilter
 import com.owncloud.android.ui.activity.FileDisplayActivity
 import com.owncloud.android.ui.preview.PreviewBitmapActivity
 import com.owncloud.android.utils.DisplayUtils
@@ -111,7 +111,7 @@ class PreviewPdfFragment : Fragment(), Injectable {
 
     override fun onPrepareOptionsMenu(menu: Menu) {
         super.onPrepareOptionsMenu(menu)
-        FileMenuFilter.hideAll(menu)
+        MenuUtils.hideAll(menu)
     }
 
     override fun onResume() {

+ 8 - 0
app/src/main/res/drawable/file_multiple.xml

@@ -0,0 +1,8 @@
+<!-- drawable/file_multiple.xml -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="24dp"
+    android:width="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path android:fillColor="#000" android:pathData="M15,7H20.5L15,1.5V7M8,0H16L22,6V18A2,2 0 0,1 20,20H8C6.89,20 6,19.1 6,18V2A2,2 0 0,1 8,0M4,4V22H20V24H4A2,2 0 0,1 2,22V4H4Z" />
+</vector>

+ 8 - 0
app/src/main/res/drawable/ic_decrypt.xml

@@ -0,0 +1,8 @@
+<!-- drawable/key_minus.xml -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="24dp"
+    android:width="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path android:fillColor="#000" android:pathData="M7.5 3C9.5 3 11.1 4.2 11.7 6H21V9H18V12H15V9H11.7C11.1 10.8 9.4 12 7.5 12C5 12 3 10 3 7.5S5 3 7.5 3M7.5 6C6.7 6 6 6.7 6 7.5S6.7 9 7.5 9 9 8.3 9 7.5 8.3 6 7.5 6M8 17H16V19H8V17Z" />
+</vector>

+ 8 - 0
app/src/main/res/drawable/ic_encrypt.xml

@@ -0,0 +1,8 @@
+<!-- drawable/key_plus.xml -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="24dp"
+    android:width="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path android:fillColor="#000" android:pathData="M7.5 3C9.5 3 11.1 4.2 11.7 6H21V9H18V12H15V9H11.7C11.1 10.8 9.4 12 7.5 12C5 12 3 10 3 7.5S5 3 7.5 3M7.5 6C6.7 6 6 6.7 6 7.5S6.7 9 7.5 9 9 8.3 9 7.5 8.3 6 7.5 6M8 17H11V14H13V17H16V19H13V22H11V19H8V17Z" />
+</vector>

+ 8 - 0
app/src/main/res/drawable/ic_export.xml

@@ -0,0 +1,8 @@
+<!-- drawable/file_export_outline.xml -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="24dp"
+    android:width="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path android:fillColor="#000" android:pathData="M14 2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V8L14 2M18 20H6V4H13V9H18V20M16 11V18.1L13.9 16L11.1 18.8L8.3 16L11.1 13.2L8.9 11H16Z" />
+</vector>

+ 8 - 0
app/src/main/res/drawable/ic_lock.xml

@@ -0,0 +1,8 @@
+<!-- drawable/lock_outline.xml -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="24dp"
+    android:width="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path android:fillColor="#000" android:pathData="M12,17C10.89,17 10,16.1 10,15C10,13.89 10.89,13 12,13A2,2 0 0,1 14,15A2,2 0 0,1 12,17M18,20V10H6V20H18M18,8A2,2 0 0,1 20,10V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V10C4,8.89 4.89,8 6,8H7V6A5,5 0 0,1 12,1A5,5 0 0,1 17,6V8H18M12,3A3,3 0 0,0 9,6V8H15V6A3,3 0 0,0 12,3Z" />
+</vector>

+ 10 - 0
app/src/main/res/drawable/ic_move.xml

@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M14 2H6C4.9 2 4 2.9 4 4V20C4 20.41 4.12 20.8 4.34 21.12C4.41 21.23 4.5 21.33 4.59 21.41C4.95 21.78 5.45 22 6 22H13.53C13 21.42 12.61 20.75 12.35 20H6V4H13V9H18V12C18.7 12 19.37 12.12 20 12.34V8L14 2M18 23L23 18.5L20 15.8L18 14V17H14V20H18V23Z" />
+</vector>

+ 10 - 0
app/src/main/res/drawable/ic_rename.xml

@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="48dp"
+    android:height="48dp"
+    android:viewportWidth="48"
+    android:viewportHeight="48"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M21.2,42 L28.2,35H43.5V42ZM38.2,14.7 L31.8,8.3 33.9,6.2Q34.75,5.35 36.025,5.375Q37.3,5.4 38.15,6.25L40.3,8.4Q41.15,9.25 41.15,10.5Q41.15,11.75 40.3,12.6ZM36.1,16.8 L10.9,42H4.5V35.6L29.7,10.4Z"/>
+</vector>

+ 8 - 0
app/src/main/res/drawable/ic_wallpaper.xml

@@ -0,0 +1,8 @@
+<!-- drawable/wallpaper.xml -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="24dp"
+    android:width="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path android:fillColor="#000" android:pathData="M4,4H11V2H4A2,2 0 0,0 2,4V11H4V4M10,13L6,18H18L15,14L12.97,16.71L10,13M17,8.5A1.5,1.5 0 0,0 15.5,7A1.5,1.5 0 0,0 14,8.5A1.5,1.5 0 0,0 15.5,10A1.5,1.5 0 0,0 17,8.5M20,2H13V4H20V11H22V4A2,2 0 0,0 20,2M20,20H13V22H20A2,2 0 0,0 22,20V13H20V20M4,13H2V20A2,2 0 0,0 4,22H11V20H4V13Z" />
+</vector>

+ 100 - 0
app/src/main/res/layout/file_actions_bottom_sheet.xml

@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Nextcloud Android client application
+  ~
+  ~  @author Álvaro Brey
+  ~  Copyright (C) 2022 Álvaro Brey
+  ~  Copyright (C) 2022 Nextcloud GmbH
+  ~
+  ~ This program is free software; you can redistribute it and/or
+  ~ modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+  ~ License as published by the Free Software Foundation; either
+  ~ version 3 of the License, or any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+  ~
+  ~ You should have received a copy of the GNU Affero General Public
+  ~ License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  ~
+  -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <LinearLayout
+        android:id="@+id/bottom_sheet"
+        android:layout_height="match_parent"
+        android:layout_width="match_parent"
+        android:orientation="vertical"
+        android:minHeight="@dimen/bottom_sheet_min_height"
+        app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
+
+        <com.google.android.material.bottomsheet.BottomSheetDragHandleView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content" />
+
+        <com.google.android.material.progressindicator.CircularProgressIndicator
+            android:indeterminate="true"
+            android:id="@+id/bottom_sheet_loading"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            tools:visibility="gone" />
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:id="@+id/bottom_sheet_content"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:orientation="vertical"
+            android:visibility="gone"
+            tools:visibility="visible">
+
+            <FrameLayout
+                android:id="@+id/thumbnail_container"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="@dimen/standard_padding"
+                app:layout_constraintBottom_toBottomOf="@+id/title"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="@+id/title">
+
+                <include
+                    android:id="@+id/thumbnail_layout"
+                    layout="@layout/file_thumbnail" />
+            </FrameLayout>
+
+            <TextView
+                android:id="@+id/title"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:ellipsize="middle"
+                android:lines="1"
+                android:padding="@dimen/standard_padding"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toEndOf="@id/thumbnail_container"
+                app:layout_constraintTop_toTopOf="parent"
+                android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall"
+                tools:text="Test file name which is very very very very very long.pdf" />
+
+            <androidx.core.widget.NestedScrollView
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/title">
+
+                <LinearLayout
+                    android:id="@+id/file_actions_list"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="vertical" />
+            </androidx.core.widget.NestedScrollView>
+        </androidx.constraintlayout.widget.ConstraintLayout>
+    </LinearLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 80 - 0
app/src/main/res/layout/file_actions_bottom_sheet_item.xml

@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Nextcloud Android client application
+  ~
+  ~  @author Álvaro Brey
+  ~  Copyright (C) 2022 Álvaro Brey
+  ~  Copyright (C) 2022 Nextcloud GmbH
+  ~
+  ~ This program is free software; you can redistribute it and/or
+  ~ modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+  ~ License as published by the Free Software Foundation; either
+  ~ version 3 of the License, or any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+  ~
+  ~ You should have received a copy of the GNU Affero General Public
+  ~ License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  ~
+  -->
+
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/menu_upload_files"
+    android:layout_width="match_parent"
+    android:layout_height="@dimen/bottom_sheet_item_height"
+    android:clickable="true"
+    android:background="?android:attr/selectableItemBackground"
+    android:gravity="center_vertical"
+    android:paddingLeft="@dimen/standard_padding"
+    android:focusable="true"
+    android:orientation="horizontal"
+    android:paddingTop="@dimen/standard_half_padding"
+    android:paddingRight="@dimen/standard_padding"
+    android:paddingBottom="@dimen/standard_half_padding"
+    tools:ignore="UseCompoundDrawables">
+
+
+    <ImageView
+        android:id="@+id/icon"
+        android:layout_width="@dimen/iconized_single_line_item_icon_size"
+        android:layout_height="@dimen/iconized_single_line_item_icon_size"
+        android:contentDescription="@null"
+        app:tint="@color/primary"
+        tools:src="@drawable/ic_delete" />
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:gravity="center_vertical"
+        android:orientation="vertical">
+
+        <TextView
+            android:id="@+id/text"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical"
+            android:layout_marginStart="@dimen/bottom_sheet_text_start_margin"
+            android:textColor="@color/text_color"
+            android:textSize="@dimen/bottom_sheet_text_size"
+            tools:text="Delete file" />
+
+        <TextView
+            android:id="@+id/text_line2"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical"
+            android:layout_marginStart="@dimen/bottom_sheet_text_start_margin"
+            android:textColor="@color/secondary_text_color"
+            android:textSize="@dimen/bottom_sheet_text_size"
+            android:visibility="gone"
+            tools:text="Some additional action info"
+            tools:visibility="visible" />
+    </LinearLayout>
+
+
+</LinearLayout>

+ 64 - 0
app/src/main/res/layout/file_thumbnail.xml

@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+
+<!--
+  ~ Nextcloud Android client application
+  ~
+  ~  @author Álvaro Brey
+  ~  Copyright (C) 2022 Álvaro Brey
+  ~  Copyright (C) 2022 Nextcloud GmbH
+  ~
+  ~ This program is free software; you can redistribute it and/or
+  ~ modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+  ~ License as published by the Free Software Foundation; either
+  ~ version 3 of the License, or any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+  ~
+  ~ You should have received a copy of the GNU Affero General Public
+  ~ License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  ~
+  -->
+
+<FrameLayout xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:ignore="MergeRootFrame"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    tools:showIn="@layout/list_item">
+
+    <FrameLayout
+        android:layout_width="@dimen/file_icon_size"
+        android:layout_height="@dimen/file_icon_size"
+        android:layout_gravity="center">
+
+        <ImageView
+            android:id="@+id/thumbnail"
+            android:layout_width="@dimen/file_icon_size"
+            android:layout_height="@dimen/file_icon_size"
+            android:contentDescription="@null"
+            android:src="@drawable/folder" />
+
+        <ImageView
+            android:id="@+id/videoOverlay"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="2dp"
+            android:layout_marginStart="2dp"
+            android:src="@drawable/video_white"
+            android:visibility="gone"
+            android:contentDescription="@string/video_overlay_icon" />
+    </FrameLayout>
+
+    <com.elyeproj.loaderviewlibrary.LoaderImageView
+        android:id="@+id/thumbnail_shimmer"
+        android:layout_width="@dimen/file_icon_size"
+        android:layout_height="@dimen/file_icon_size"
+        android:visibility="gone"
+        app:corners="8" />
+</FrameLayout>
+

+ 12 - 40
app/src/main/res/layout/list_item.xml

@@ -18,7 +18,6 @@
  -->
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
     android:id="@+id/ListItemLayout"
     android:layout_width="match_parent"
     android:layout_height="@dimen/standard_list_item_size"
@@ -34,7 +33,7 @@
         android:layout_marginBottom="@dimen/standard_padding">
 
         <FrameLayout
-            android:id="@+id/thumbnail_layout"
+            android:id="@+id/thumbnail_container"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             app:layout_constraintBottom_toBottomOf="parent"
@@ -42,36 +41,9 @@
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintTop_toTopOf="parent">
 
-            <FrameLayout
-                android:layout_width="@dimen/file_icon_size"
-                android:layout_height="@dimen/file_icon_size"
-                android:layout_gravity="center">
-
-                <ImageView
-                    android:id="@+id/thumbnail"
-                    android:layout_width="@dimen/file_icon_size"
-                    android:layout_height="@dimen/file_icon_size"
-                    android:contentDescription="@null"
-                    android:src="@drawable/folder" />
-
-                <ImageView
-                    android:id="@+id/videoOverlay"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_marginTop="2dp"
-                    android:layout_marginStart="2dp"
-                    android:src="@drawable/video_white"
-                    android:visibility="gone"
-                    tools:visibility="visible"
-                    android:contentDescription="@string/video_overlay_icon" />
-            </FrameLayout>
-
-            <com.elyeproj.loaderviewlibrary.LoaderImageView
-                android:id="@+id/thumbnail_shimmer"
-                android:layout_width="@dimen/file_icon_size"
-                android:layout_height="@dimen/file_icon_size"
-                android:visibility="gone"
-                app:corners="8" />
+            <include
+                android:id="@+id/thumbnail_layout"
+                layout="@layout/file_thumbnail" />
         </FrameLayout>
 
         <ImageView
@@ -81,10 +53,10 @@
             android:contentDescription="@string/downloader_download_succeeded_ticker"
             android:scaleType="fitCenter"
             android:src="@drawable/ic_synced"
-            app:layout_constraintBottom_toBottomOf="@+id/thumbnail_layout"
-            app:layout_constraintEnd_toEndOf="@+id/thumbnail_layout"
-            app:layout_constraintStart_toEndOf="@+id/thumbnail_layout"
-            app:layout_constraintTop_toBottomOf="@+id/thumbnail_layout" />
+            app:layout_constraintBottom_toBottomOf="@+id/thumbnail_container"
+            app:layout_constraintEnd_toEndOf="@+id/thumbnail_container"
+            app:layout_constraintStart_toEndOf="@+id/thumbnail_container"
+            app:layout_constraintTop_toBottomOf="@+id/thumbnail_container" />
 
         <ImageView
             android:id="@+id/favorite_action"
@@ -92,10 +64,10 @@
             android:layout_height="@dimen/list_item_favorite_action_layout_height"
             android:contentDescription="@string/favorite"
             android:src="@drawable/favorite"
-            app:layout_constraintBottom_toTopOf="@+id/thumbnail_layout"
-            app:layout_constraintEnd_toEndOf="@+id/thumbnail_layout"
-            app:layout_constraintStart_toEndOf="@+id/thumbnail_layout"
-            app:layout_constraintTop_toTopOf="@+id/thumbnail_layout" />
+            app:layout_constraintBottom_toTopOf="@+id/thumbnail_container"
+            app:layout_constraintEnd_toEndOf="@+id/thumbnail_container"
+            app:layout_constraintStart_toEndOf="@+id/thumbnail_container"
+            app:layout_constraintTop_toTopOf="@+id/thumbnail_container" />
 
     </androidx.constraintlayout.widget.ConstraintLayout>
 

+ 30 - 0
app/src/main/res/menu/custom_menu_placeholder.xml

@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Nextcloud Android client application
+  ~
+  ~  @author Álvaro Brey
+  ~  Copyright (C) 2022 Álvaro Brey
+  ~  Copyright (C) 2022 Nextcloud GmbH
+  ~
+  ~ This program is free software; you can redistribute it and/or
+  ~ modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+  ~ License as published by the Free Software Foundation; either
+  ~ version 3 of the License, or any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+  ~
+  ~ You should have received a copy of the GNU Affero General Public
+  ~ License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  ~
+  -->
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+    <!-- This is a "fake" menu so that we can implement custom actions on menu click, while still behaving and looking like a traditional menu item -->
+    <item
+        android:id="@+id/custom_menu_placeholder_item"
+        android:icon="@drawable/ic_dots_vertical"
+        android:title="@string/overflow_menu"
+        app:showAsAction="always" />
+</menu>

+ 0 - 82
app/src/main/res/menu/fragment_file_detail.xml

@@ -1,82 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  Nextcloud Android client application
-
-  Copyright (C) 2018 Andy Scherzinger
-
-  This program is free software; you can redistribute it and/or
-  modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
-  License as published by the Free Software Foundation; either
-  version 3 of the License, or any later version.
-
-  This program is distributed in the hope that it will be useful,
-  but WITHOUT ANY WARRANTY; without even the implied warranty of
-  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-  GNU AFFERO GENERAL PUBLIC LICENSE for more details.
-
-  You should have received a copy of the GNU Affero General Public
-  License along with this program.  If not, see <http://www.gnu.org/licenses/>.
--->
-<menu xmlns:android="http://schemas.android.com/apk/res/android"
-      xmlns:app="http://schemas.android.com/apk/res-auto"
-      xmlns:tools="http://schemas.android.com/tools"
-      tools:ignore="AppCompatResource">
-
-    <item
-        android:id="@+id/action_rename_file"
-        android:title="@string/common_rename"
-        app:showAsAction="never"
-        android:showAsAction="never" />
-    <item
-        android:id="@+id/action_download_file"
-        android:title="@string/filedetails_download"
-        app:showAsAction="never"
-        android:showAsAction="never" />
-    <item
-        android:id="@+id/action_export_file"
-        android:title="@string/filedetails_export"
-        app:showAsAction="never"
-        android:showAsAction="never" />
-    <item
-        android:id="@+id/action_send_file"
-        android:title="@string/common_send"
-        app:showAsAction="never"
-        android:showAsAction="never"
-        android:icon="@drawable/ic_share" />
-    <item
-        android:id="@+id/action_open_file_with"
-        android:title="@string/actionbar_open_with"
-        app:showAsAction="never"
-        android:showAsAction="never" />
-    <item
-        android:id="@+id/action_sync_file"
-        android:title="@string/filedetails_sync_file"
-        app:showAsAction="never"
-        android:showAsAction="never" />
-    <item
-        android:id="@+id/action_cancel_sync"
-        android:title="@string/common_cancel_sync"
-        app:showAsAction="never"
-        android:showAsAction="never" />
-    <item
-        android:id="@+id/action_encrypted"
-        android:title="@string/encrypted"
-        app:showAsAction="never"
-        android:showAsAction="never" />
-    <item
-        android:id="@+id/action_unset_encrypted"
-        android:title="@string/unset_encrypted"
-        app:showAsAction="never"
-        android:showAsAction="never" />
-    <item
-        android:id="@+id/action_set_as_wallpaper"
-        android:title="@string/set_picture_as"
-        app:showAsAction="never"
-        android:showAsAction="never" />
-    <item
-        android:id="@+id/action_remove_file"
-        android:title="@string/common_remove"
-        app:showAsAction="never"
-        android:showAsAction="never" />
-
-</menu>

+ 0 - 190
app/src/main/res/menu/item_file.xml

@@ -1,190 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ownCloud Android client application
-
-  Copyright (C) 2012 Bartek Przybylski
-  Copyright (C) 2015 ownCloud Inc.
-
-  This program is free software: you can redistribute it and/or modify
-  it under the terms of the GNU General Public License version 2,
-  as published by the Free Software Foundation.
-
-  This program is distributed in the hope that it will be useful,
-  but WITHOUT ANY WARRANTY; without even the implied warranty of
-  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-  GNU General Public License for more details.
-
-  You should have received a copy of the GNU General Public License
-  along with this program.  If not, see <http://www.gnu.org/licenses/>.
--->
-<menu xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
-    tools:ignore="AppCompatResource">
-
-    <group
-        android:id="@+id/menu_group_lock_info"
-        android:visible="false"
-        android:enabled="false"
-        tools:visible="true">
-
-        <item
-            android:id="@+id/action_locked_by"
-            android:showAsAction="never"
-            android:title="Locked by %1$s"
-            android:enabled="false"
-            app:showAsAction="never"
-            tools:ignore="HardcodedText"
-            tools:title="Locked by Username With Surname at bla bla bla bla bla" />
-
-        <item
-            android:id="@+id/action_locked_until"
-            android:showAsAction="never"
-            android:enabled="false"
-            android:title="Lock expires: %1$s"
-            app:showAsAction="never"
-            tools:ignore="HardcodedText"
-            tools:title="Lock expires: in 20 minutes" />
-    </group>
-
-    <item
-        android:id="@+id/action_unlock_file"
-        android:showAsAction="never"
-        android:title="@string/unlock_file"
-        app:showAsAction="never" />
-
-    <item
-        android:id="@+id/action_edit"
-        android:title="@string/action_edit"
-        app:showAsAction="never"
-        android:showAsAction="never" />
-
-    <item
-        android:id="@+id/action_favorite"
-        android:title="@string/favorite"
-        app:showAsAction="never"
-        android:showAsAction="never" />
-
-    <item
-        android:id="@+id/action_unset_favorite"
-        android:title="@string/unset_favorite"
-        app:showAsAction="never"
-        android:showAsAction="never" />
-
-    <item
-        android:id="@+id/action_see_details"
-        android:title="@string/actionbar_see_details"
-        app:showAsAction="never"
-        android:showAsAction="never" />
-
-    <item
-        android:id="@+id/action_lock_file"
-        android:showAsAction="never"
-        android:title="@string/lock_file"
-        app:showAsAction="never" />
-
-    <item
-        android:id="@+id/action_rename_file"
-        android:title="@string/common_rename"
-        app:showAsAction="never"
-        android:showAsAction="never" />
-
-    <item
-        android:id="@+id/action_move"
-        android:title="@string/actionbar_move"
-        app:showAsAction="never"
-        android:showAsAction="never" />
-
-    <item
-        android:id="@+id/action_copy"
-        android:title="@string/actionbar_copy"
-        app:showAsAction="never"
-        android:showAsAction="never" />
-
-    <item
-        android:id="@+id/action_download_file"
-        android:title="@string/filedetails_download"
-        app:showAsAction="never"
-        android:showAsAction="never" />
-
-    <item
-        android:id="@+id/action_export_file"
-        android:title="@string/filedetails_export"
-        app:showAsAction="never"
-        android:showAsAction="never" />
-
-    <item
-        android:id="@+id/action_stream_media"
-        android:title="@string/stream"
-        app:showAsAction="never"
-        android:showAsAction="never"
-        android:orderInCategory="1" />
-
-    <item
-        android:id="@+id/action_send_share_file"
-        android:title="@string/action_send_share"
-        app:showAsAction="never"
-        android:showAsAction="never"
-        android:icon="@drawable/ic_share" />
-
-    <item
-        android:id="@+id/action_send_file"
-        android:title="@string/common_send"
-        app:showAsAction="never"
-        android:showAsAction="never"
-        android:icon="@drawable/ic_share" />
-
-    <item
-        android:id="@+id/action_open_file_with"
-        android:title="@string/actionbar_open_with"
-        app:showAsAction="never"
-        android:showAsAction="never" />
-
-    <item
-        android:id="@+id/action_sync_file"
-        android:title="@string/filedetails_sync_file"
-        app:showAsAction="never"
-        android:showAsAction="never" />
-    <item
-        android:id="@+id/action_cancel_sync"
-        android:title="@string/common_cancel_sync"
-        app:showAsAction="never"
-        android:showAsAction="never" />
-
-    <item
-        android:id="@+id/action_select_all_action_menu"
-        android:title="@string/select_all"
-        app:showAsAction="never"
-        android:showAsAction="never" />
-
-    <item
-        android:id="@+id/action_deselect_all_action_menu"
-        android:title="@string/deselect_all"
-        app:showAsAction="never"
-        android:showAsAction="never" />
-
-    <item
-        android:id="@+id/action_encrypted"
-        android:title="@string/encrypted"
-        app:showAsAction="never"
-        android:showAsAction="never" />
-
-    <item
-        android:id="@+id/action_unset_encrypted"
-        android:title="@string/unset_encrypted"
-        app:showAsAction="never"
-        android:showAsAction="never" />
-
-    <item
-        android:id="@+id/action_set_as_wallpaper"
-        android:title="@string/set_picture_as"
-        app:showAsAction="never"
-        android:showAsAction="never" />
-
-    <item
-        android:id="@+id/action_remove_file"
-        android:title="@string/common_remove"
-        app:showAsAction="never"
-        android:showAsAction="never" />
-
-</menu>

+ 1 - 0
app/src/main/res/values/dims.xml

@@ -23,6 +23,7 @@
     <dimen name="bottom_sheet_text_start_margin">40dp</dimen>
     <dimen name="bottom_sheet_item_height">56dp</dimen>
     <dimen name="bottom_sheet_menu_item_divider_standard_margin">80dp</dimen>
+    <dimen name="bottom_sheet_min_height">112dp</dimen>
     <dimen name="file_icon_size">40dp</dimen>
     <dimen name="file_icon_size_grid">128dp</dimen>
     <dimen name="file_icon_rounded_corner_radius">8dp</dimen>

+ 49 - 0
app/src/main/res/values/ids.xml

@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Nextcloud Android client application
+  ~
+  ~  @author Álvaro Brey
+  ~  Copyright (C) 2022 Álvaro Brey
+  ~  Copyright (C) 2022 Nextcloud GmbH
+  ~
+  ~ This program is free software; you can redistribute it and/or
+  ~ modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+  ~ License as published by the Free Software Foundation; either
+  ~ version 3 of the License, or any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+  ~
+  ~ You should have received a copy of the GNU Affero General Public
+  ~ License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  ~
+  -->
+
+<resources>
+    <item name="action_locked_by" type="id"/>
+    <item name="action_locked_until" type="id"/>
+    <item name="action_unlock_file" type="id"/>
+    <item name="action_edit" type="id"/>
+    <item name="action_favorite" type="id"/>
+    <item name="action_unset_favorite" type="id"/>
+    <item name="action_see_details" type="id"/>
+    <item name="action_lock_file" type="id"/>
+    <item name="action_rename_file" type="id"/>
+    <item name="action_move" type="id"/>
+    <item name="action_copy" type="id"/>
+    <item name="action_download_file" type="id"/>
+    <item name="action_export_file" type="id"/>
+    <item name="action_stream_media" type="id"/>
+    <item name="action_send_share_file" type="id"/>
+    <item name="action_send_file" type="id"/>
+    <item name="action_open_file_with" type="id"/>
+    <item name="action_sync_file" type="id"/>
+    <item name="action_cancel_sync" type="id"/>
+    <item name="action_select_all_action_menu" type="id"/>
+    <item name="action_deselect_all_action_menu" type="id"/>
+    <item name="action_encrypted" type="id"/>
+    <item name="action_unset_encrypted" type="id"/>
+    <item name="action_set_as_wallpaper" type="id"/>
+    <item name="action_remove_file" type="id"/>
+</resources>

+ 1 - 0
app/src/main/res/values/strings.xml

@@ -1053,4 +1053,5 @@
     <string name="no_items">No items</string>
     <string name="check_back_later_or_reload">Check back later or reload.</string>
     <string name="e2e_not_yet_setup">E2E not yet setup</string>
+    <string name="error_file_actions">Error showing file actions</string>
 </resources>

+ 1 - 0
app/src/main/res/values/styles.xml

@@ -256,6 +256,7 @@
         <item name="android:navigationBarColor">@color/black</item>
         <item name="toolbarStyle">@style/Theme.ownCloud.Overlay.ActionBar</item>
         <item name="actionOverflowButtonStyle">@style/ToolbarStyle.Overflow</item>
+        <item name="bottomSheetDialogTheme">@style/ThemeOverlay.App.BottomSheetDialog</item>
     </style>
 
     <style name="Theme.ownCloud.OverlayGrey" parent="Theme.ownCloud.OverlayBase">