Переглянути джерело

Reimplement BrowserController with new architecture pattern

Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Andy Scherzinger 2 роки тому
батько
коміт
a41d14c33a
23 змінених файлів з 1536 додано та 23 видалено
  1. 5 1
      app/src/main/AndroidManifest.xml
  2. 6 6
      app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java
  3. 6 6
      app/src/main/java/com/nextcloud/talk/components/filebrowser/operations/DavListing.java
  4. 3 2
      app/src/main/java/com/nextcloud/talk/components/filebrowser/webdav/LegacyReadFilesystemOperation.java
  5. 184 0
      app/src/main/java/com/nextcloud/talk/components/filebrowser/webdav/ReadFilesystemOperation.kt
  6. 16 8
      app/src/main/java/com/nextcloud/talk/controllers/ProfileController.kt
  7. 9 0
      app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt
  8. 6 0
      app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt
  9. 345 0
      app/src/main/java/com/nextcloud/talk/remotefilebrowser/activities/RemoteFileBrowserActivity.kt
  10. 86 0
      app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsAdapter.kt
  11. 153 0
      app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsListViewHolder.kt
  12. 53 0
      app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsViewHolder.kt
  13. 49 0
      app/src/main/java/com/nextcloud/talk/remotefilebrowser/model/RemoteFileBrowserItem.kt
  14. 29 0
      app/src/main/java/com/nextcloud/talk/remotefilebrowser/repositories/RemoteFileBrowserItemsRepository.kt
  15. 56 0
      app/src/main/java/com/nextcloud/talk/remotefilebrowser/repositories/RemoteFileBrowserItemsRepositoryImpl.kt
  16. 93 0
      app/src/main/java/com/nextcloud/talk/remotefilebrowser/viewmodels/RemoteFileBrowserItemsViewModel.kt
  17. 19 0
      app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java
  18. 52 0
      app/src/main/java/com/nextcloud/talk/utils/FileSortOrderByDateNew.java
  19. 63 0
      app/src/main/java/com/nextcloud/talk/utils/FileSortOrderByNameNew.java
  20. 61 0
      app/src/main/java/com/nextcloud/talk/utils/FileSortOrderBySizeNew.java
  21. 108 0
      app/src/main/java/com/nextcloud/talk/utils/FileSortOrderNew.java
  22. 2 0
      app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt
  23. 132 0
      app/src/main/res/layout/activity_remote_file_browser.xml

+ 5 - 1
app/src/main/AndroidManifest.xml

@@ -170,7 +170,11 @@
 
         <activity
             android:name=".shareditems.activities.SharedItemsActivity"
-            android:theme="@style/AppTheme"/>
+            android:theme="@style/AppTheme" />
+
+        <activity
+            android:name=".remotefilebrowser.activities.RemoteFileBrowserActivity"
+            android:theme="@style/AppTheme" />
 
         <activity
             android:name=".messagesearch.MessageSearchActivity"

+ 6 - 6
app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java

@@ -45,7 +45,7 @@ import com.nextcloud.talk.R;
 import com.nextcloud.talk.application.NextcloudTalkApplication;
 import com.nextcloud.talk.components.filebrowser.models.BrowserFile;
 import com.nextcloud.talk.components.filebrowser.models.DavResponse;
-import com.nextcloud.talk.components.filebrowser.webdav.ReadFilesystemOperation;
+import com.nextcloud.talk.components.filebrowser.webdav.LegacyReadFilesystemOperation;
 import com.nextcloud.talk.databinding.ReactionsInsideMessageBinding;
 import com.nextcloud.talk.models.database.UserEntity;
 import com.nextcloud.talk.models.json.chat.ChatMessage;
@@ -289,20 +289,20 @@ public abstract class MagicPreviewMessageViewHolder extends MessageHolders.Incom
     }
 
     private void fetchFileInformation(String url, UserEntity activeUser) {
-        Single.fromCallable(new Callable<ReadFilesystemOperation>() {
+        Single.fromCallable(new Callable<LegacyReadFilesystemOperation>() {
             @Override
-            public ReadFilesystemOperation call() {
-                return new ReadFilesystemOperation(okHttpClient, activeUser, url, 0);
+            public LegacyReadFilesystemOperation call() {
+                return new LegacyReadFilesystemOperation(okHttpClient, activeUser, url, 0);
             }
         }).observeOn(Schedulers.io())
-            .subscribe(new SingleObserver<ReadFilesystemOperation>() {
+            .subscribe(new SingleObserver<LegacyReadFilesystemOperation>() {
                 @Override
                 public void onSubscribe(@NonNull Disposable d) {
                     // unused atm
                 }
 
                 @Override
-                public void onSuccess(@NonNull ReadFilesystemOperation readFilesystemOperation) {
+                public void onSuccess(@NonNull LegacyReadFilesystemOperation readFilesystemOperation) {
                     DavResponse davResponse = readFilesystemOperation.readRemotePath();
                     if (davResponse.data != null) {
                         List<BrowserFile> browserFileList = (List<BrowserFile>) davResponse.data;

+ 6 - 6
app/src/main/java/com/nextcloud/talk/components/filebrowser/operations/DavListing.java

@@ -26,7 +26,7 @@ import android.util.Log;
 
 import com.nextcloud.talk.components.filebrowser.interfaces.ListingInterface;
 import com.nextcloud.talk.components.filebrowser.models.DavResponse;
-import com.nextcloud.talk.components.filebrowser.webdav.ReadFilesystemOperation;
+import com.nextcloud.talk.components.filebrowser.webdav.LegacyReadFilesystemOperation;
 import com.nextcloud.talk.models.database.UserEntity;
 
 import java.util.concurrent.Callable;
@@ -50,20 +50,20 @@ public class DavListing extends ListingAbstractClass {
 
     @Override
     public void getFiles(String path, UserEntity currentUser, @Nullable OkHttpClient okHttpClient) {
-        Single.fromCallable(new Callable<ReadFilesystemOperation>() {
+        Single.fromCallable(new Callable<LegacyReadFilesystemOperation>() {
             @Override
-            public ReadFilesystemOperation call() {
-                return new ReadFilesystemOperation(okHttpClient, currentUser, path, 1);
+            public LegacyReadFilesystemOperation call() {
+                return new LegacyReadFilesystemOperation(okHttpClient, currentUser, path, 1);
             }
         }).subscribeOn(Schedulers.io())
-                .subscribe(new SingleObserver<ReadFilesystemOperation>() {
+                .subscribe(new SingleObserver<LegacyReadFilesystemOperation>() {
                     @Override
                     public void onSubscribe(@NonNull Disposable d) {
 
                     }
 
                     @Override
-                    public void onSuccess(@NonNull ReadFilesystemOperation readFilesystemOperation) {
+                    public void onSuccess(@NonNull LegacyReadFilesystemOperation readFilesystemOperation) {
                         davResponse = readFilesystemOperation.readRemotePath();
                         try {
                             listingInterface.listingResult(davResponse);

+ 3 - 2
app/src/main/java/com/nextcloud/talk/components/filebrowser/webdav/ReadFilesystemOperation.java → app/src/main/java/com/nextcloud/talk/components/filebrowser/webdav/LegacyReadFilesystemOperation.java

@@ -40,14 +40,15 @@ import kotlin.jvm.functions.Function2;
 import okhttp3.HttpUrl;
 import okhttp3.OkHttpClient;
 
-public class ReadFilesystemOperation {
+@Deprecated
+public class LegacyReadFilesystemOperation {
     private static final String TAG = "ReadFilesystemOperation";
     private final OkHttpClient okHttpClient;
     private final String url;
     private final int depth;
     private final String basePath;
 
-    public ReadFilesystemOperation(OkHttpClient okHttpClient, UserEntity currentUser, String path, int depth) {
+    public LegacyReadFilesystemOperation(OkHttpClient okHttpClient, UserEntity currentUser, String path, int depth) {
         OkHttpClient.Builder okHttpClientBuilder = okHttpClient.newBuilder();
         okHttpClientBuilder.followRedirects(false);
         okHttpClientBuilder.followSslRedirects(false);

+ 184 - 0
app/src/main/java/com/nextcloud/talk/components/filebrowser/webdav/ReadFilesystemOperation.kt

@@ -0,0 +1,184 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Andy Scherzinger
+ * @author Mario Danic
+ * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
+ * Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.components.filebrowser.webdav
+
+import android.net.Uri
+import android.text.TextUtils
+import android.util.Log
+import at.bitfire.dav4jvm.DavResource
+import at.bitfire.dav4jvm.Property
+import at.bitfire.dav4jvm.Response
+import at.bitfire.dav4jvm.Response.HrefRelation
+import at.bitfire.dav4jvm.exception.DavException
+import at.bitfire.dav4jvm.property.DisplayName
+import at.bitfire.dav4jvm.property.GetContentType
+import at.bitfire.dav4jvm.property.GetLastModified
+import at.bitfire.dav4jvm.property.ResourceType
+import com.nextcloud.talk.components.filebrowser.models.DavResponse
+import com.nextcloud.talk.components.filebrowser.models.properties.NCEncrypted
+import com.nextcloud.talk.components.filebrowser.models.properties.NCPermission
+import com.nextcloud.talk.components.filebrowser.models.properties.NCPreview
+import com.nextcloud.talk.components.filebrowser.models.properties.OCFavorite
+import com.nextcloud.talk.components.filebrowser.models.properties.OCId
+import com.nextcloud.talk.components.filebrowser.models.properties.OCSize
+import com.nextcloud.talk.dagger.modules.RestModule.MagicAuthenticator
+import com.nextcloud.talk.models.database.UserEntity
+import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem
+import com.nextcloud.talk.utils.ApiUtils
+import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
+import okhttp3.OkHttpClient
+import java.io.File
+import java.io.IOException
+
+class ReadFilesystemOperation(okHttpClient: OkHttpClient, currentUser: UserEntity, path: String, depth: Int) {
+    private val okHttpClient: OkHttpClient
+    private val url: String
+    private val depth: Int
+    private val basePath: String
+    fun readRemotePath(): DavResponse {
+        val davResponse = DavResponse()
+        val memberElements: MutableList<Response> = ArrayList()
+        val rootElement = arrayOfNulls<Response>(1)
+        val remoteFiles: MutableList<RemoteFileBrowserItem> = ArrayList()
+        try {
+            DavResource(
+                okHttpClient,
+                url.toHttpUrlOrNull()!!
+            ).propfind(
+                depth = depth,
+                reqProp = DavUtils.getAllPropSet()
+            ) { response: Response, hrefRelation: HrefRelation? ->
+                davResponse.setResponse(response)
+                when (hrefRelation) {
+                    HrefRelation.MEMBER -> memberElements.add(response)
+                    HrefRelation.SELF -> rootElement[0] = response
+                    HrefRelation.OTHER -> {}
+                    else -> {}
+                }
+                Unit
+            }
+        } catch (e: IOException) {
+            Log.w(TAG, "Error reading remote path")
+        } catch (e: DavException) {
+            Log.w(TAG, "Error reading remote path")
+        }
+        remoteFiles.add(
+            getModelFromResponse(
+                rootElement[0]!!,
+                rootElement[0]!!
+                    .href
+                    .toString()
+                    .substring(basePath.length)
+            )
+        )
+        for (memberElement in memberElements) {
+            remoteFiles.add(
+                getModelFromResponse(
+                    memberElement,
+                    memberElement
+                        .href
+                        .toString()
+                        .substring(basePath.length)
+                )
+            )
+        }
+        davResponse.setData(remoteFiles)
+        return davResponse
+    }
+
+    companion object {
+        private const val TAG = "ReadFilesystemOperation"
+    }
+
+    init {
+        val okHttpClientBuilder: OkHttpClient.Builder = okHttpClient.newBuilder()
+        okHttpClientBuilder.followRedirects(false)
+        okHttpClientBuilder.followSslRedirects(false)
+        okHttpClientBuilder.authenticator(
+            MagicAuthenticator(
+                ApiUtils.getCredentials(
+                    currentUser.username,
+                    currentUser.token
+                ),
+                "Authorization"
+            )
+        )
+        this.okHttpClient = okHttpClientBuilder.build()
+        basePath = currentUser.baseUrl + DavUtils.DAV_PATH + currentUser.userId
+        url = basePath + path
+        this.depth = depth
+    }
+
+    private fun getModelFromResponse(response: Response, remotePath: String): RemoteFileBrowserItem {
+        val remoteFileBrowserItem = RemoteFileBrowserItem()
+        remoteFileBrowserItem.path = Uri.decode(remotePath)
+        remoteFileBrowserItem.displayName = Uri.decode(File(remotePath).name)
+        val properties = response.properties
+        for (property in properties) {
+            mapPropertyToBrowserFile(property, remoteFileBrowserItem)
+        }
+        if (remoteFileBrowserItem.permissions != null && remoteFileBrowserItem.permissions!!.contains("R")) {
+            remoteFileBrowserItem.isAllowedToReShare = true
+        }
+        if (TextUtils.isEmpty(remoteFileBrowserItem.mimeType) && !remoteFileBrowserItem.isFile) {
+            remoteFileBrowserItem.mimeType = "inode/directory"
+        }
+
+        return remoteFileBrowserItem
+    }
+
+    @Suppress("Detekt.ComplexMethod")
+    private fun mapPropertyToBrowserFile(property: Property, remoteFileBrowserItem: RemoteFileBrowserItem) {
+        when (property) {
+            is OCId -> {
+                remoteFileBrowserItem.remoteId = property.ocId
+            }
+            is ResourceType -> {
+                remoteFileBrowserItem.isFile = !property.types.contains(ResourceType.COLLECTION)
+            }
+            is GetLastModified -> {
+                remoteFileBrowserItem.modifiedTimestamp = property.lastModified
+            }
+            is GetContentType -> {
+                remoteFileBrowserItem.mimeType = property.type
+            }
+            is OCSize -> {
+                remoteFileBrowserItem.size = property.ocSize
+            }
+            is NCPreview -> {
+                remoteFileBrowserItem.hasPreview = property.isNcPreview
+            }
+            is OCFavorite -> {
+                remoteFileBrowserItem.isFavorite = property.isOcFavorite
+            }
+            is DisplayName -> {
+                remoteFileBrowserItem.displayName = property.displayName
+            }
+            is NCEncrypted -> {
+                remoteFileBrowserItem.isEncrypted = property.isNcEncrypted
+            }
+            is NCPermission -> {
+                remoteFileBrowserItem.permissions = property.ncPermission
+            }
+        }
+    }
+}

+ 16 - 8
app/src/main/java/com/nextcloud/talk/controllers/ProfileController.kt

@@ -29,7 +29,6 @@ import android.graphics.BitmapFactory
 import android.graphics.Color
 import android.net.Uri
 import android.os.Bundle
-import android.os.Environment
 import android.text.Editable
 import android.text.TextUtils
 import android.text.TextWatcher
@@ -48,8 +47,6 @@ import androidx.core.graphics.drawable.DrawableCompat
 import androidx.core.view.ViewCompat
 import androidx.recyclerview.widget.RecyclerView
 import autodagger.AutoInjector
-import com.bluelinelabs.conductor.RouterTransaction
-import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler
 import com.github.dhaval2404.imagepicker.ImagePicker
 import com.github.dhaval2404.imagepicker.ImagePicker.Companion.getError
 import com.github.dhaval2404.imagepicker.ImagePicker.Companion.getFile
@@ -59,7 +56,6 @@ import com.nextcloud.talk.api.NcApi
 import com.nextcloud.talk.application.NextcloudTalkApplication
 import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
 import com.nextcloud.talk.components.filebrowser.controllers.BrowserController.BrowserType
-import com.nextcloud.talk.components.filebrowser.controllers.BrowserForAvatarController
 import com.nextcloud.talk.controllers.base.NewBaseController
 import com.nextcloud.talk.controllers.util.viewBinding
 import com.nextcloud.talk.databinding.ControllerProfileBinding
@@ -71,11 +67,15 @@ import com.nextcloud.talk.models.json.userprofile.Scope
 import com.nextcloud.talk.models.json.userprofile.UserProfileData
 import com.nextcloud.talk.models.json.userprofile.UserProfileFieldsOverall
 import com.nextcloud.talk.models.json.userprofile.UserProfileOverall
+import com.nextcloud.talk.remotefilebrowser.activities.RemoteFileBrowserActivity
 import com.nextcloud.talk.ui.dialog.ScopeDialog
 import com.nextcloud.talk.utils.ApiUtils
 import com.nextcloud.talk.utils.DisplayUtils
+import com.nextcloud.talk.utils.FileUtils
 import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_BROWSER_TYPE
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_MIME_TYPE_FILTER
 import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SINGLE_SELECTION
 import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY
 import com.nextcloud.talk.utils.database.user.UserUtils
 import io.reactivex.Observer
@@ -496,12 +496,22 @@ class ProfileController : NewBaseController(R.layout.controller_profile) {
             KEY_USER_ENTITY,
             Parcels.wrap(UserEntity::class.java, currentUser)
         )
+        bundle.putBoolean(KEY_SINGLE_SELECTION, true)
+        bundle.putString(KEY_MIME_TYPE_FILTER, "image/")
         bundle.putString(KEY_ROOM_TOKEN, "123")
+
+        val avatarIntent = Intent(activity, RemoteFileBrowserActivity::class.java)
+        avatarIntent.putExtras(bundle)
+
+        startActivityForResult(avatarIntent, RemoteFileBrowserActivity.REQUEST_CODE_SELECT_AVATAR)
+
+        /*
         router.pushController(
             RouterTransaction.with(BrowserForAvatarController(bundle, this))
                 .pushChangeHandler(VerticalChangeHandler())
                 .popChangeHandler(VerticalChangeHandler())
         )
+        */
     }
 
     fun handleAvatar(remotePath: String?) {
@@ -526,10 +536,7 @@ class ProfileController : NewBaseController(R.layout.controller_profile) {
     private fun saveBitmapAndPassToImagePicker(bitmap: Bitmap) {
         var file: File? = null
         try {
-            file = File.createTempFile(
-                "avatar", "png",
-                Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)
-            )
+            file = FileUtils.getTempCacheFile(context!!, "avatar/avatar.png")
             try {
                 FileOutputStream(file).use { out -> bitmap.compress(Bitmap.CompressFormat.PNG, FULL_QUALITY, out) }
             } catch (e: IOException) {
@@ -555,6 +562,7 @@ class ProfileController : NewBaseController(R.layout.controller_profile) {
 
     override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
         if (resultCode == Activity.RESULT_OK) {
+
             uploadAvatar(getFile(data))
         } else if (resultCode == ImagePicker.RESULT_ERROR) {
             Toast.makeText(activity, getError(data), Toast.LENGTH_SHORT).show()

+ 9 - 0
app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt

@@ -22,6 +22,8 @@
 package com.nextcloud.talk.dagger.modules
 
 import com.nextcloud.talk.api.NcApi
+import com.nextcloud.talk.remotefilebrowser.repositories.RemoteFileBrowserItemsRepository
+import com.nextcloud.talk.remotefilebrowser.repositories.RemoteFileBrowserItemsRepositoryImpl
 import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepository
 import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepositoryImpl
 import com.nextcloud.talk.shareditems.repositories.SharedItemsRepository
@@ -29,6 +31,7 @@ import com.nextcloud.talk.shareditems.repositories.SharedItemsRepositoryImpl
 import com.nextcloud.talk.utils.database.user.CurrentUserProvider
 import dagger.Module
 import dagger.Provides
+import okhttp3.OkHttpClient
 
 @Module
 class RepositoryModule {
@@ -41,4 +44,10 @@ class RepositoryModule {
     fun provideUnifiedSearchRepository(ncApi: NcApi, userProvider: CurrentUserProvider): UnifiedSearchRepository {
         return UnifiedSearchRepositoryImpl(ncApi, userProvider)
     }
+
+    @Provides
+    fun provideRemoteFileBrowserItemsRepository(okHttpClient: OkHttpClient, userProvider: CurrentUserProvider):
+        RemoteFileBrowserItemsRepository {
+        return RemoteFileBrowserItemsRepositoryImpl(okHttpClient, userProvider)
+    }
 }

+ 6 - 0
app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt

@@ -23,6 +23,7 @@ package com.nextcloud.talk.dagger.modules
 
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.ViewModelProvider
+import com.nextcloud.talk.remotefilebrowser.viewmodels.RemoteFileBrowserItemsViewModel
 import com.nextcloud.talk.messagesearch.MessageSearchViewModel
 import com.nextcloud.talk.shareditems.viewmodels.SharedItemsViewModel
 import dagger.Binds
@@ -59,4 +60,9 @@ abstract class ViewModelModule {
     @IntoMap
     @ViewModelKey(MessageSearchViewModel::class)
     abstract fun messageSearchViewModel(viewModel: MessageSearchViewModel): ViewModel
+
+    @Binds
+    @IntoMap
+    @ViewModelKey(RemoteFileBrowserItemsViewModel::class)
+    abstract fun remoteFileBrowserItemsViewModel(viewModel: RemoteFileBrowserItemsViewModel): ViewModel
 }

+ 345 - 0
app/src/main/java/com/nextcloud/talk/remotefilebrowser/activities/RemoteFileBrowserActivity.kt

@@ -0,0 +1,345 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Andy Scherzinger
+ * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.remotefilebrowser.activities
+
+import android.app.Activity
+import android.content.Intent
+import android.os.Bundle
+import android.util.Log
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.res.ResourcesCompat
+import androidx.fragment.app.DialogFragment
+import androidx.lifecycle.ViewModelProvider
+import androidx.recyclerview.widget.GridLayoutManager
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+import autodagger.AutoInjector
+import com.nextcloud.talk.R
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.databinding.ActivityRemoteFileBrowserBinding
+import com.nextcloud.talk.interfaces.SelectionInterface
+import com.nextcloud.talk.remotefilebrowser.adapters.RemoteFileBrowserItemsAdapter
+import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem
+import com.nextcloud.talk.remotefilebrowser.viewmodels.RemoteFileBrowserItemsViewModel
+import com.nextcloud.talk.ui.dialog.SortingOrderDialogFragment
+import com.nextcloud.talk.utils.DisplayUtils
+import com.nextcloud.talk.utils.FileSortOrder
+import com.nextcloud.talk.utils.FileSortOrderNew
+import com.nextcloud.talk.utils.database.user.UserUtils
+import com.nextcloud.talk.utils.preferences.AppPreferences
+import net.orange_box.storebox.listeners.OnPreferenceValueChangedListener
+import java.io.File
+import java.util.Collections
+import java.util.TreeSet
+import javax.inject.Inject
+
+@AutoInjector(NextcloudTalkApplication::class)
+class RemoteFileBrowserActivity : AppCompatActivity(), SelectionInterface, SwipeRefreshLayout.OnRefreshListener {
+
+    @Inject
+    lateinit var viewModelFactory: ViewModelProvider.Factory
+
+    @Inject
+    lateinit var appPreferences: AppPreferences
+
+    @Inject
+    lateinit var userUtils: UserUtils
+
+    private lateinit var binding: ActivityRemoteFileBrowserBinding
+    private lateinit var viewModel: RemoteFileBrowserItemsViewModel
+
+    private var filesSelectionDoneMenuItem: MenuItem? = null
+
+    private val selectedPaths: MutableSet<String> = Collections.synchronizedSet(TreeSet())
+    private var currentPath: String = "/"
+
+    private var browserItems: List<RemoteFileBrowserItem> = emptyList()
+    private var adapter: RemoteFileBrowserItemsAdapter? = null
+
+    private var sortingChangeListener: OnPreferenceValueChangedListener<String>? = null
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
+
+        binding = ActivityRemoteFileBrowserBinding.inflate(layoutInflater)
+        setSupportActionBar(binding.remoteFileBrowserItemsToolbar)
+        setContentView(binding.root)
+
+        DisplayUtils.applyColorToStatusBar(
+            this,
+            ResourcesCompat.getColor(
+                resources, R.color.appbar, null
+            )
+        )
+        DisplayUtils.applyColorToNavigationBar(
+            this.window,
+            ResourcesCompat.getColor(resources, R.color.bg_default, null)
+        )
+
+        supportActionBar?.title = "current patch"
+        supportActionBar?.setDisplayHomeAsUpEnabled(true)
+
+        initViewModel()
+
+        binding.swipeRefreshList.setOnRefreshListener(this)
+        binding.swipeRefreshList.setColorSchemeResources(R.color.colorPrimary)
+        binding.swipeRefreshList.setProgressBackgroundColorSchemeResource(R.color.refresh_spinner_background)
+
+        appPreferences.registerSortingChangeListener(
+            SortingChangeListener(this).also {
+                sortingChangeListener = it
+            }
+        )
+
+        viewModel.loadItems(currentPath)
+    }
+
+    private fun initViewModel() {
+        viewModel = ViewModelProvider(this, viewModelFactory)[RemoteFileBrowserItemsViewModel::class.java]
+
+        viewModel.viewState.observe(this) { state ->
+            clearEmptyLoading()
+            when (state) {
+                is RemoteFileBrowserItemsViewModel.LoadingItemsState, RemoteFileBrowserItemsViewModel.InitialState -> {
+                    showLoading()
+                }
+
+                is RemoteFileBrowserItemsViewModel.NoRemoteFileItemsState -> {
+                    showEmpty()
+                }
+
+                is RemoteFileBrowserItemsViewModel.LoadedState -> {
+                    val remoteFileBrowserItems = state.items
+                    Log.d(TAG, "Items received: $remoteFileBrowserItems")
+
+                    // TODO make shwoGrid based on preferences
+                    val showGrid = false
+                    val layoutManager = if (showGrid) {
+                        GridLayoutManager(this, SPAN_COUNT)
+                    } else {
+                        LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
+                    }
+
+                    // TODO make mimeTypeSelectionFilter a bundled arg for the activity
+                    val mimeTypeSelectionFilter = "image/"
+                    val adapter = RemoteFileBrowserItemsAdapter(
+                        showGrid = showGrid,
+                        mimeTypeSelectionFilter = mimeTypeSelectionFilter,
+                        userEntity = userUtils.currentUser!!,
+                        selectionInterface = this
+                    ) { remoteFileBrowserItem ->
+                        onItemClicked(remoteFileBrowserItem)
+                    }
+                        .apply {
+                            items = if (remoteFileBrowserItems.size > 1) {
+                                remoteFileBrowserItems.subList(1, remoteFileBrowserItems.size)
+                            } else {
+                                ArrayList()
+                            }
+                            browserItems = items
+                        }
+
+                    binding.recyclerView.adapter = adapter
+                    binding.recyclerView.layoutManager = layoutManager
+                    binding.recyclerView.setHasFixedSize(true)
+
+                    showList()
+                }
+            }
+        }
+    }
+
+    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+        super.onCreateOptionsMenu(menu)
+        menuInflater.inflate(R.menu.menu_share_files, menu)
+        filesSelectionDoneMenuItem = menu?.findItem(R.id.files_selection_done)
+        filesSelectionDoneMenuItem?.isVisible = selectedPaths.size > 0
+        return true
+    }
+
+    private fun onItemClicked(remoteFileBrowserItem: RemoteFileBrowserItem) {
+        if ("inode/directory" == remoteFileBrowserItem.mimeType) {
+            currentPath = remoteFileBrowserItem.path!!
+            viewModel.loadItems(currentPath)
+        } else {
+            toggleBrowserItemSelection(remoteFileBrowserItem.path!!)
+        }
+    }
+
+    override fun onResume() {
+        super.onResume()
+
+        binding.pathNavigationBackButton.setOnClickListener { goBack() }
+        binding.sortButton.setOnClickListener { changeSorting() }
+
+        binding.sortButton.setText(
+            DisplayUtils.getSortOrderStringId(FileSortOrder.getFileSortOrder(appPreferences.sorting))
+        )
+
+        refreshCurrentPath()
+    }
+
+    fun changeSorting() {
+        val newFragment: DialogFragment = SortingOrderDialogFragment
+            .newInstance(FileSortOrder.getFileSortOrder(appPreferences.sorting))
+        newFragment.show(
+            supportFragmentManager,
+            SortingOrderDialogFragment.SORTING_ORDER_FRAGMENT
+        )
+    }
+
+    private fun goBack(): Boolean {
+        if (currentPath != "/") {
+            viewModel.loadItems(File(currentPath).parent!!)
+        }
+        return true
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        return when (item.itemId) {
+            android.R.id.home -> {
+                onBackPressed()
+                true
+            }
+            R.id.files_selection_done -> {
+                onFileSelectionDone()
+                true
+            }
+            else -> {
+                return super.onOptionsItemSelected(item)
+            }
+        }
+    }
+
+    private fun onFileSelectionDone() {
+        val data = Intent()
+        data.putStringArrayListExtra(EXTRA_SELECTED_PATHS, ArrayList(selectedPaths))
+        setResult(Activity.RESULT_OK, data)
+        finish()
+    }
+
+    private fun clearEmptyLoading() {
+        binding.emptyContainer.emptyListView.visibility = View.GONE
+    }
+
+    private fun showLoading() {
+        binding.emptyContainer.emptyListViewHeadline.text = getString(R.string.file_list_loading)
+        binding.emptyContainer.emptyListView.visibility = View.VISIBLE
+        binding.recyclerView.visibility = View.GONE
+    }
+
+    private fun showEmpty() {
+        binding.emptyContainer.emptyListViewHeadline.text = getString(R.string.nc_shared_items_empty)
+        binding.emptyContainer.emptyListView.visibility = View.VISIBLE
+        binding.recyclerView.visibility = View.GONE
+    }
+
+    private fun showList() {
+        binding.recyclerView.visibility = View.VISIBLE
+    }
+
+    override fun onRefresh() {
+        refreshCurrentPath()
+    }
+
+    private fun refreshCurrentPath() {
+        viewModel.loadItems(currentPath)
+    }
+
+    private fun shouldPathBeSelectedDueToParent(currentPath: String): Boolean {
+        var file = File(currentPath)
+        if (selectedPaths.size > 0 && file.parent != "/") {
+            while (file.parent != null) {
+                var parent = file.parent!!
+                if (File(file.parent!!).parent != null) {
+                    parent += "/"
+                }
+                if (selectedPaths.contains(parent)) {
+                    return true
+                }
+                file = File(file.parent!!)
+            }
+        }
+        return false
+    }
+
+    private fun checkAndRemoveAnySelectedParents(currentPath: String) {
+        var file = File(currentPath)
+        selectedPaths.remove(currentPath)
+        while (file.parent != null) {
+            selectedPaths.remove(file.parent!! + "/")
+            file = File(file.parent!!)
+        }
+        runOnUiThread {
+            binding.recyclerView.adapter!!.notifyDataSetChanged()
+        }
+    }
+
+    companion object {
+        private val TAG = RemoteFileBrowserActivity::class.simpleName
+        const val SPAN_COUNT: Int = 4
+        const val EXTRA_SELECTED_PATHS = "EXTRA_SELECTED_PATH"
+        const val REQUEST_CODE_SELECT_AVATAR = 22
+    }
+
+    override fun toggleBrowserItemSelection(path: String) {
+        if (selectedPaths.contains(path) || shouldPathBeSelectedDueToParent(path)) {
+            checkAndRemoveAnySelectedParents(path)
+        } else {
+            // TODO if it's a folder, remove all the children we added manually
+            selectedPaths.add(path)
+        }
+        filesSelectionDoneMenuItem?.isVisible = selectedPaths.size > 0
+    }
+
+    override fun isPathSelected(path: String): Boolean {
+        return selectedPaths.contains(path) || shouldPathBeSelectedDueToParent(path)
+    }
+
+    override fun shouldOnlySelectOneImageFile(): Boolean {
+        return true
+    }
+
+    @Suppress("Detekt.TooGenericExceptionCaught")
+    private class SortingChangeListener(private val activity: RemoteFileBrowserActivity) :
+        OnPreferenceValueChangedListener<String> {
+        override fun onChanged(newValue: String) {
+            try {
+                val sortOrder = FileSortOrderNew.getFileSortOrder(newValue)
+
+                activity.binding.sortButton.setText(DisplayUtils.getSortOrderStringId(sortOrder))
+                activity.browserItems = sortOrder.sortCloudFiles(activity.browserItems)
+
+                activity.runOnUiThread {
+                    activity.adapter!!.updateDataSet(activity.browserItems)
+                }
+            } catch (npe: NullPointerException) {
+                // view binding can be null
+                // since this is called asynchronously and UI might have been destroyed in the meantime
+                Log.i(TAG, "UI destroyed - view binding already gone")
+            }
+        }
+    }
+}

+ 86 - 0
app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsAdapter.kt

@@ -0,0 +1,86 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Andy Scherzinger
+ * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.remotefilebrowser.adapters
+
+import android.annotation.SuppressLint
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import com.nextcloud.talk.databinding.RvItemBrowserFileBinding
+import com.nextcloud.talk.interfaces.SelectionInterface
+import com.nextcloud.talk.models.database.UserEntity
+import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem
+
+class RemoteFileBrowserItemsAdapter(
+    private val showGrid: Boolean = false,
+    private val mimeTypeSelectionFilter: String? = null,
+    private val userEntity: UserEntity,
+    private val selectionInterface: SelectionInterface,
+    private val onItemClicked: (RemoteFileBrowserItem) -> Unit
+) : RecyclerView.Adapter<RemoteFileBrowserItemsViewHolder>() {
+
+    var items: List<RemoteFileBrowserItem> = emptyList()
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RemoteFileBrowserItemsViewHolder {
+
+        return if (showGrid) {
+            RemoteFileBrowserItemsListViewHolder(
+                RvItemBrowserFileBinding.inflate(
+                    LayoutInflater.from(parent.context),
+                    parent,
+                    false
+                ),
+                mimeTypeSelectionFilter,
+                userEntity,
+                selectionInterface
+            ) {
+                onItemClicked(items[it])
+            }
+        } else {
+            RemoteFileBrowserItemsListViewHolder(
+                RvItemBrowserFileBinding.inflate(
+                    LayoutInflater.from(parent.context),
+                    parent,
+                    false
+                ),
+                mimeTypeSelectionFilter,
+                userEntity,
+                selectionInterface
+            ) {
+                onItemClicked(items[it])
+            }
+        }
+    }
+
+    override fun onBindViewHolder(holder: RemoteFileBrowserItemsViewHolder, position: Int) {
+        holder.onBind(items[position])
+    }
+
+    override fun getItemCount(): Int {
+        return items.size
+    }
+
+    @SuppressLint("NotifyDataSetChanged")
+    fun updateDataSet(browserItems: List<RemoteFileBrowserItem>) {
+        items = browserItems
+        notifyDataSetChanged()
+    }
+}

+ 153 - 0
app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsListViewHolder.kt

@@ -0,0 +1,153 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Andy Scherzinger
+ * Copyright (C) 202 Andy Scherzinger <info@andy-scherzinger.de>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.remotefilebrowser.adapters
+
+import android.text.format.Formatter
+import android.util.Log
+import android.view.View
+import androidx.appcompat.content.res.AppCompatResources
+import autodagger.AutoInjector
+import com.facebook.drawee.backends.pipeline.Fresco
+import com.facebook.drawee.interfaces.DraweeController
+import com.facebook.drawee.view.SimpleDraweeView
+import com.nextcloud.talk.R
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.databinding.RvItemBrowserFileBinding
+import com.nextcloud.talk.interfaces.SelectionInterface
+import com.nextcloud.talk.models.database.UserEntity
+import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem
+import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.DateUtils.getLocalDateTimeStringFromTimestamp
+import com.nextcloud.talk.utils.DisplayUtils
+import com.nextcloud.talk.utils.DrawableUtils.getDrawableResourceIdForMimeType
+
+@AutoInjector(NextcloudTalkApplication::class)
+class RemoteFileBrowserItemsListViewHolder(
+    override val binding: RvItemBrowserFileBinding,
+    mimeTypeSelectionFilter: String?,
+    currentUser: UserEntity,
+    selectionInterface: SelectionInterface,
+    onItemClicked: (Int) -> Unit
+) : RemoteFileBrowserItemsViewHolder(binding, mimeTypeSelectionFilter, currentUser, selectionInterface) {
+
+    override val fileIcon: SimpleDraweeView
+        get() = binding.fileIcon
+
+    private var selectable : Boolean = true
+    private var clickable : Boolean = true
+
+    init {
+        itemView.setOnClickListener {
+            if (clickable) {
+                onItemClicked(bindingAdapterPosition)
+                if (selectable) {
+                    binding.selectFileCheckbox.toggle()
+                }
+            }
+        }
+    }
+
+    override fun onBind(item: RemoteFileBrowserItem) {
+
+        super.onBind(item)
+
+        binding.fileIcon.controller = null
+        if (!item.isAllowedToReShare || item.isEncrypted) {
+            binding.root.isEnabled = false
+            binding.root.alpha = DISABLED_ALPHA
+        } else {
+            binding.root.isEnabled = true
+            binding.root.alpha = ENABLED_ALPHA
+        }
+
+        binding.fileEncryptedImageView.visibility =
+            if (item.isEncrypted) {
+                View.VISIBLE
+            } else {
+                View.GONE
+            }
+
+        binding.fileFavoriteImageView.visibility =
+            if (item.isFavorite) {
+                View.VISIBLE
+            } else {
+                View.GONE
+            }
+
+        calculateSelectability(item)
+        calculateClickability(item, selectable)
+        setSelectability()
+
+        binding.fileIcon
+            .hierarchy
+            .setPlaceholderImage(
+                AppCompatResources.getDrawable(
+                    binding.fileIcon.context, getDrawableResourceIdForMimeType(item.mimeType)
+                )
+            )
+
+        if (item.hasPreview) {
+            val path = ApiUtils.getUrlForFilePreviewWithRemotePath(
+                currentUser.baseUrl,
+                item.path,
+                binding.fileIcon.context.resources.getDimensionPixelSize(R.dimen.small_item_height)
+            )
+            if (path.isNotEmpty()) {
+                val draweeController: DraweeController = Fresco.newDraweeControllerBuilder()
+                    .setAutoPlayAnimations(true)
+                    .setImageRequest(DisplayUtils.getImageRequestForUrl(path, null))
+                    .build()
+                binding.fileIcon.controller = draweeController
+            }
+        }
+
+        binding.filenameTextView.text = item.displayName
+        binding.fileModifiedInfo.text = String.format(
+            binding.fileModifiedInfo.context.getString(R.string.nc_last_modified),
+            Formatter.formatShortFileSize(binding.fileModifiedInfo.context, item.size),
+            getLocalDateTimeStringFromTimestamp(item.modifiedTimestamp)
+        )
+
+        binding.selectFileCheckbox.isChecked = selectionInterface.isPathSelected(item.path!!)
+    }
+
+    private fun setSelectability() {
+        if (selectable) {
+            binding.selectFileCheckbox.visibility = View.VISIBLE
+        } else {
+            binding.selectFileCheckbox.visibility = View.GONE
+        }
+    }
+
+    private fun calculateSelectability(item: RemoteFileBrowserItem) {
+        selectable = item.isFile &&
+            (mimeTypeSelectionFilter == null || item.mimeType?.startsWith(mimeTypeSelectionFilter) == true)
+    }
+
+    private fun calculateClickability(item: RemoteFileBrowserItem, selectableItem: Boolean) {
+        clickable = selectableItem || "inode/directory" == item.mimeType
+    }
+
+    companion object {
+        private const val DISABLED_ALPHA: Float = 0.38f
+        private const val ENABLED_ALPHA: Float = 1.0f
+    }
+}

+ 53 - 0
app/src/main/java/com/nextcloud/talk/remotefilebrowser/adapters/RemoteFileBrowserItemsViewHolder.kt

@@ -0,0 +1,53 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Andy Scherzinger
+ * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.remotefilebrowser.adapters
+
+import android.graphics.drawable.Drawable
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.RecyclerView
+import androidx.viewbinding.ViewBinding
+import com.facebook.drawee.view.SimpleDraweeView
+import com.nextcloud.talk.interfaces.SelectionInterface
+import com.nextcloud.talk.models.database.UserEntity
+import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem
+import com.nextcloud.talk.utils.DrawableUtils
+
+abstract class RemoteFileBrowserItemsViewHolder(
+    open val binding: ViewBinding,
+    val mimeTypeSelectionFilter: String? = null,
+    val currentUser: UserEntity,
+    val selectionInterface: SelectionInterface,
+) : RecyclerView.ViewHolder(binding.root) {
+
+    abstract val fileIcon: SimpleDraweeView
+
+    open fun onBind(item: RemoteFileBrowserItem) {
+        fileIcon.hierarchy.setPlaceholderImage(staticImage(item.mimeType, fileIcon))
+    }
+
+    private fun staticImage(
+        mimeType: String?,
+        image: SimpleDraweeView
+    ): Drawable {
+        val drawableResourceId = DrawableUtils.getDrawableResourceIdForMimeType(mimeType)
+        return ContextCompat.getDrawable(image.context, drawableResourceId)!!
+    }
+}

+ 49 - 0
app/src/main/java/com/nextcloud/talk/remotefilebrowser/model/RemoteFileBrowserItem.kt

@@ -0,0 +1,49 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Andy Scherzinger
+ * @author Mario Danic
+ * Copyright (C) 202 Andy Scherzinger <info@andy-scherzinger.de>
+ * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.remotefilebrowser.model
+
+import android.os.Parcelable
+import com.bluelinelabs.logansquare.annotation.JsonObject
+import kotlinx.android.parcel.Parcelize
+
+@Parcelize
+@JsonObject
+data class RemoteFileBrowserItem(
+    var path: String? = null,
+    var displayName: String? = null,
+    var mimeType: String? = null,
+    var modifiedTimestamp: Long = 0,
+    var size: Long = 0,
+    var isFile: Boolean = false,
+
+    // Used for remote files
+    var remoteId: String? = null,
+    var hasPreview: Boolean = false,
+    var isFavorite: Boolean = false,
+    var isEncrypted: Boolean = false,
+    var permissions: String? = null,
+    var isAllowedToReShare: Boolean = false
+) : Parcelable {
+    // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
+    constructor() : this(null, null, null, 0, 0, false, null, false, false, false, null, false)
+}

+ 29 - 0
app/src/main/java/com/nextcloud/talk/remotefilebrowser/repositories/RemoteFileBrowserItemsRepository.kt

@@ -0,0 +1,29 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Andy Scherzinger
+ * Copyright (C) 202 Andy Scherzinger <info@andy-scherzinger.de>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.remotefilebrowser.repositories
+
+import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem
+import io.reactivex.Observable
+
+interface RemoteFileBrowserItemsRepository {
+
+    fun listFolder(path: String): Observable<List<RemoteFileBrowserItem>>
+}

+ 56 - 0
app/src/main/java/com/nextcloud/talk/remotefilebrowser/repositories/RemoteFileBrowserItemsRepositoryImpl.kt

@@ -0,0 +1,56 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Andy Scherzinger
+ * Copyright (C) 202 Andy Scherzinger <info@andy-scherzinger.de>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.remotefilebrowser.repositories
+
+import com.nextcloud.talk.components.filebrowser.webdav.ReadFilesystemOperation
+import com.nextcloud.talk.models.database.UserEntity
+import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem
+import com.nextcloud.talk.utils.database.user.CurrentUserProvider
+import io.reactivex.Observable
+import okhttp3.OkHttpClient
+import javax.inject.Inject
+
+class RemoteFileBrowserItemsRepositoryImpl @Inject constructor(
+    private val okHttpClient: OkHttpClient,
+    private val userProvider: CurrentUserProvider
+) : RemoteFileBrowserItemsRepository {
+
+    private val userEntity: UserEntity
+        get() = userProvider.currentUser!!
+
+    override fun listFolder(path: String):
+        Observable<List<RemoteFileBrowserItem>> {
+        return Observable.fromCallable {
+            val operation =
+                ReadFilesystemOperation(
+                    okHttpClient,
+                    userEntity,
+                    path,
+                    1
+                )
+            val davResponse = operation.readRemotePath()
+            if (davResponse.getData() != null) {
+                return@fromCallable davResponse.getData() as List<RemoteFileBrowserItem>
+            }
+            return@fromCallable emptyList()
+        }
+    }
+}

+ 93 - 0
app/src/main/java/com/nextcloud/talk/remotefilebrowser/viewmodels/RemoteFileBrowserItemsViewModel.kt

@@ -0,0 +1,93 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Andy Scherzinger
+ * Copyright (C) 202 Andy Scherzinger <info@andy-scherzinger.de>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.remotefilebrowser.viewmodels
+
+import android.util.Log
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem
+import com.nextcloud.talk.remotefilebrowser.repositories.RemoteFileBrowserItemsRepository
+import io.reactivex.Observer
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.disposables.Disposable
+import io.reactivex.schedulers.Schedulers
+import javax.inject.Inject
+
+class RemoteFileBrowserItemsViewModel @Inject constructor(
+    private val repository: RemoteFileBrowserItemsRepository
+) :
+    ViewModel() {
+
+    sealed interface ViewState
+    object InitialState : ViewState
+    object NoRemoteFileItemsState : ViewState
+    object LoadingItemsState : ViewState
+    class LoadedState(val items: List<RemoteFileBrowserItem>) : ViewState
+
+    private val _viewState: MutableLiveData<ViewState> = MutableLiveData(InitialState)
+
+    val viewState: LiveData<ViewState>
+        get() = _viewState
+
+    fun loadItems(path: String) {
+        _viewState.value = LoadingItemsState
+        repository.listFolder(path).subscribeOn(Schedulers.io())
+            ?.observeOn(AndroidSchedulers.mainThread())
+            ?.subscribe(RemoteFileBrowserItemsObserver())
+    }
+
+    inner class RemoteFileBrowserItemsObserver : Observer<List<RemoteFileBrowserItem>> {
+
+        var newRemoteFileBrowserItems: List<RemoteFileBrowserItem>? = null
+
+        override fun onSubscribe(d: Disposable) = Unit
+
+        override fun onNext(response: List<RemoteFileBrowserItem>) {
+            newRemoteFileBrowserItems = response
+        }
+
+        override fun onError(e: Throwable) {
+            Log.d(TAG, "An error occurred: $e")
+        }
+
+        override fun onComplete() {
+            if (newRemoteFileBrowserItems.isNullOrEmpty()) {
+                this@RemoteFileBrowserItemsViewModel._viewState.value = NoRemoteFileItemsState
+            } else {
+                setCurrentState(newRemoteFileBrowserItems!!)
+            }
+        }
+
+        private fun setCurrentState(items: List<RemoteFileBrowserItem>) {
+            when (this@RemoteFileBrowserItemsViewModel._viewState.value) {
+                is LoadedState, LoadingItemsState -> {
+                    this@RemoteFileBrowserItemsViewModel._viewState.value = LoadedState(items)
+                }
+                else -> return
+            }
+        }
+    }
+
+    companion object {
+        private val TAG = RemoteFileBrowserItemsViewModel::class.simpleName
+    }
+}

+ 19 - 0
app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java

@@ -636,6 +636,25 @@ public class DisplayUtils {
         }
     }
 
+    public static @StringRes
+    int getSortOrderStringId(FileSortOrderNew sortOrder) {
+        switch (sortOrder.name) {
+            case sort_z_to_a_id:
+                return R.string.menu_item_sort_by_name_z_a;
+            case sort_new_to_old_id:
+                return R.string.menu_item_sort_by_date_newest_first;
+            case sort_old_to_new_id:
+                return R.string.menu_item_sort_by_date_oldest_first;
+            case sort_big_to_small_id:
+                return R.string.menu_item_sort_by_size_biggest_first;
+            case sort_small_to_big_id:
+                return R.string.menu_item_sort_by_size_smallest_first;
+            case sort_a_to_z_id:
+            default:
+                return R.string.menu_item_sort_by_name_a_z;
+        }
+    }
+
     /**
      * calculates the relative time string based on the given modification timestamp.
      *

+ 52 - 0
app/src/main/java/com/nextcloud/talk/utils/FileSortOrderByDateNew.java

@@ -0,0 +1,52 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Sven R. Kunze
+ * @author Andy Scherzinger
+ * Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
+ * Copyright (C) 2017 Sven R. Kunze
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.utils;
+
+import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Created by srkunze on 28.08.17.
+ */
+public class FileSortOrderByDateNew extends FileSortOrderNew {
+
+    FileSortOrderByDateNew(String name, boolean ascending) {
+        super(name, ascending);
+    }
+
+    /**
+     * Sorts list by Date.
+     *
+     * @param files list of files to sort
+     */
+    public List<RemoteFileBrowserItem> sortCloudFiles(List<RemoteFileBrowserItem> files) {
+        final int multiplier = isAscending ? 1 : -1;
+
+        Collections.sort(files, (o1, o2) ->
+                multiplier * Long.compare(o1.getModifiedTimestamp(), o2.getModifiedTimestamp()));
+
+        return super.sortCloudFiles(files);
+    }
+}

+ 63 - 0
app/src/main/java/com/nextcloud/talk/utils/FileSortOrderByNameNew.java

@@ -0,0 +1,63 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Sven R. Kunze
+ * @author Andy Scherzinger
+ * Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
+ * Copyright (C) 2017 Sven R. Kunze
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.utils;
+
+import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem;
+
+import java.util.Collections;
+import java.util.List;
+
+import third_parties.daveKoeller.AlphanumComparator;
+
+/**
+ * Created by srkunze on 28.08.17.
+ */
+public class FileSortOrderByNameNew extends FileSortOrderNew {
+
+    FileSortOrderByNameNew(String name, boolean ascending) {
+        super(name, ascending);
+    }
+
+    /**
+     * Sorts list by Name.
+     *
+     * @param files files to sort
+     */
+    @SuppressWarnings("Bx")
+    public List<RemoteFileBrowserItem> sortCloudFiles(List<RemoteFileBrowserItem> files) {
+        final int multiplier = isAscending ? 1 : -1;
+
+        Collections.sort(files, (o1, o2) -> {
+            if (!o1.isFile() && !o2.isFile()) {
+                return multiplier * new AlphanumComparator().compare(o1, o2);
+            } else if (!o1.isFile()) {
+                return -1;
+            } else if (!o2.isFile()) {
+                return 1;
+            }
+            return multiplier * new AlphanumComparator().compare(o1, o2);
+        });
+
+        return super.sortCloudFiles(files);
+    }
+}

+ 61 - 0
app/src/main/java/com/nextcloud/talk/utils/FileSortOrderBySizeNew.java

@@ -0,0 +1,61 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Sven R. Kunze
+ * @author Andy Scherzinger
+ * Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
+ * Copyright (C) 2017 Sven R. Kunze
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.utils;
+
+import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Sorts files by sizes
+ */
+public class FileSortOrderBySizeNew extends FileSortOrderNew {
+
+    FileSortOrderBySizeNew(String name, boolean ascending) {
+        super(name, ascending);
+    }
+
+    /**
+     * Sorts list by Size.
+     *
+     * @param files list of files to sort
+     */
+    public List<RemoteFileBrowserItem> sortCloudFiles(List<RemoteFileBrowserItem> files) {
+        final int multiplier = isAscending ? 1 : -1;
+
+        Collections.sort(files, (o1, o2) -> {
+            if (!o1.isFile() && !o2.isFile()) {
+                return multiplier * Long.compare(o1.getSize(), o2.getSize());
+            } else if (!o1.isFile()) {
+                return -1;
+            } else if (!o2.isFile()) {
+                return 1;
+            } else {
+                return multiplier * Long.compare(o1.getSize(), o2.getSize());
+            }
+        });
+
+        return super.sortCloudFiles(files);
+    }
+}

+ 108 - 0
app/src/main/java/com/nextcloud/talk/utils/FileSortOrderNew.java

@@ -0,0 +1,108 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Sven R. Kunze
+ * @author Andy Scherzinger
+ * Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
+ * Copyright (C) 2017 Sven R. Kunze
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.utils;
+
+import android.text.TextUtils;
+
+import com.nextcloud.talk.components.filebrowser.adapters.items.BrowserFileItem;
+import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Sort order
+ */
+public class FileSortOrderNew {
+    public static final String sort_a_to_z_id = "sort_a_to_z";
+    public static final String sort_z_to_a_id = "sort_z_to_a";
+    public static final String sort_old_to_new_id = "sort_old_to_new";
+    public static final String sort_new_to_old_id = "sort_new_to_old";
+    public static final String sort_small_to_big_id = "sort_small_to_big";
+    public static final String sort_big_to_small_id = "sort_big_to_small";
+
+    public static final FileSortOrderNew sort_a_to_z = new FileSortOrderByNameNew(sort_a_to_z_id, true);
+    public static final FileSortOrderNew sort_z_to_a = new FileSortOrderByNameNew(sort_z_to_a_id, false);
+    public static final FileSortOrderNew sort_old_to_new = new FileSortOrderByDateNew(sort_old_to_new_id, true);
+    public static final FileSortOrderNew sort_new_to_old = new FileSortOrderByDateNew(sort_new_to_old_id, false);
+    public static final FileSortOrderNew sort_small_to_big = new FileSortOrderBySizeNew(sort_small_to_big_id, true);
+    public static final FileSortOrderNew sort_big_to_small = new FileSortOrderBySizeNew(sort_big_to_small_id, false);
+
+    public static final Map<String, FileSortOrderNew> sortOrders;
+
+    static {
+        HashMap<String, FileSortOrderNew> temp = new HashMap<>();
+        temp.put(sort_a_to_z.name, sort_a_to_z);
+        temp.put(sort_z_to_a.name, sort_z_to_a);
+        temp.put(sort_old_to_new.name, sort_old_to_new);
+        temp.put(sort_new_to_old.name, sort_new_to_old);
+        temp.put(sort_small_to_big.name, sort_small_to_big);
+        temp.put(sort_big_to_small.name, sort_big_to_small);
+
+        sortOrders = Collections.unmodifiableMap(temp);
+    }
+
+    public String name;
+    public boolean isAscending;
+
+    public FileSortOrderNew(String name, boolean ascending) {
+        this.name = name;
+        isAscending = ascending;
+    }
+
+    public static FileSortOrderNew getFileSortOrder(@Nullable String key) {
+        if (TextUtils.isEmpty(key) || !sortOrders.containsKey(key)) {
+            return sort_a_to_z;
+        } else {
+            return sortOrders.get(key);
+        }
+    }
+
+    public List<RemoteFileBrowserItem> sortCloudFiles(List<RemoteFileBrowserItem> files) {
+        return sortCloudFilesByFavourite(files);
+    }
+
+    /**
+     * Sorts list by Favourites.
+     *
+     * @param files files to sort
+     */
+    public static List<RemoteFileBrowserItem> sortCloudFilesByFavourite(List<RemoteFileBrowserItem> files) {
+        Collections.sort(files, (o1, o2) -> {
+            if (o1.isFavorite() && o2.isFavorite()) {
+                return 0;
+            } else if (o1.isFavorite()) {
+                return -1;
+            } else if (o2.isFavorite()) {
+                return 1;
+            }
+            return 0;
+        });
+
+        return files;
+    }
+}

+ 2 - 0
app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt

@@ -74,4 +74,6 @@ object BundleKeys {
     val KEY_FORWARD_HIDE_SOURCE_ROOM = "KEY_FORWARD_HIDE_SOURCE_ROOM"
     val KEY_SYSTEM_NOTIFICATION_ID = "KEY_SYSTEM_NOTIFICATION_ID"
     const val KEY_MESSAGE_ID = "KEY_MESSAGE_ID"
+    const val KEY_SINGLE_SELECTION = "KEY_SINGLE_SELECTION"
+    const val KEY_MIME_TYPE_FILTER = "KEY_MIME_TYPE_FILTER"
 }

+ 132 - 0
app/src/main/res/layout/activity_remote_file_browser.xml

@@ -0,0 +1,132 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Nextcloud Talk application
+  ~
+  ~ @author Mario Danic
+  ~ @author Andy Scherzinger
+  ~ Copyright (C) 2021-2022 Andy Scherzinger <info@andy-scherzinger.de>
+  ~ Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
+  ~
+  ~ 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 <http://www.gnu.org/licenses/>.
+  -->
+
+<androidx.coordinatorlayout.widget.CoordinatorLayout 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"
+    android:background="@color/bg_default"
+    tools:context=".remotefilebrowser.activities.RemoteFileBrowserActivity">
+
+    <com.google.android.material.appbar.AppBarLayout
+        android:id="@+id/remote_file_browser_items_appbar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+
+        <com.google.android.material.appbar.MaterialToolbar
+            android:id="@+id/remote_file_browser_items_toolbar"
+            android:layout_width="match_parent"
+            android:layout_height="?attr/actionBarSize"
+            android:background="@color/appbar"
+            android:theme="?attr/actionBarPopupTheme"
+            app:layout_scrollFlags="scroll|enterAlways"
+            app:navigationIconTint="@color/fontAppbar"
+            app:popupTheme="@style/appActionBarPopupMenu"
+            app:titleTextColor="@color/fontAppbar"
+            tools:title="@string/nc_app_product_name" />
+
+        <!-- sorting/layout bar -->
+        <RelativeLayout
+            android:id="@+id/sort_list_button_group"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:background="@color/appbar"
+            android:visibility="visible"
+            tools:visibility="visible">
+
+            <com.google.android.material.button.MaterialButton
+                android:id="@+id/sort_button"
+                style="@style/Nextcloud.Material.TextButton"
+                android:layout_width="wrap_content"
+                android:layout_height="@dimen/min_size_clickable_area"
+                android:layout_marginStart="7dp"
+                android:contentDescription=""
+                android:text="@string/menu_item_sort_by_date_newest_first"
+                android:textAlignment="textStart"
+                android:textAllCaps="false"
+                android:textColor="@color/fontAppbar"
+                android:textSize="14sp"
+                app:icon="@drawable/ic_keyboard_arrow_down"
+                app:iconGravity="textEnd"
+                app:iconSize="16dp"
+                app:iconTint="@color/fontAppbar" />
+
+            <com.google.android.material.button.MaterialButton
+                android:id="@+id/switch_grid_view_button"
+                style="@style/Widget.AppTheme.Button.IconButton"
+                android:layout_width="@dimen/min_size_clickable_area"
+                android:layout_height="@dimen/min_size_clickable_area"
+                android:layout_alignEnd="@+id/sort_button"
+                android:layout_alignParentEnd="true"
+                android:layout_marginEnd="1dp"
+                android:contentDescription=""
+                android:visibility="invisible"
+                app:cornerRadius="24dp"
+                app:icon="@drawable/ic_search_grey"
+                app:iconTint="@color/fontAppbar" />
+
+            <com.google.android.material.button.MaterialButton
+                android:id="@+id/path_navigation_back_button"
+                style="@style/Nextcloud.Material.TextButton"
+                android:layout_width="wrap_content"
+                android:layout_height="@dimen/min_size_clickable_area"
+                android:layout_below="@id/sort_button"
+                android:layout_centerInParent="true"
+                android:contentDescription=""
+                android:text="@string/nc_file_browser_back"
+                android:textAlignment="textStart"
+                android:textAllCaps="false"
+                android:textColor="@color/fontAppbar"
+                android:textSize="14sp"
+                app:icon="@drawable/ic_arrow_back_black_24dp"
+                app:iconGravity="textStart"
+                app:iconPadding="@dimen/standard_half_padding"
+                app:iconSize="16dp"
+                app:iconTint="@color/fontAppbar" />
+
+        </RelativeLayout>
+
+    </com.google.android.material.appbar.AppBarLayout>
+
+    <include
+        android:id="@+id/emptyContainer"
+        layout="@layout/empty_list"
+        android:visibility="gone"
+        tools:visibility="visible" />
+
+    <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+        android:id="@+id/swipe_refresh_list"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        app:layout_behavior="@string/appbar_scrolling_view_behavior"
+        android:footerDividersEnabled="false">
+
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/recycler_view"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            tools:listitem="@layout/rv_item_browser_file" />
+
+    </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
+
+</androidx.coordinatorlayout.widget.CoordinatorLayout>