Эх сурвалжийг харах

wip

Signed-off-by: tobiasKaminsky <tobias@kaminsky.me>
tobiasKaminsky 4 жил өмнө
parent
commit
3a39b041de
20 өөрчлөгдсөн 2022 нэмэгдсэн , 9 устгасан
  1. 1 1
      settings.gradle
  2. 88 0
      src/androidTest/java/com/owncloud/android/ui/fragment/UnifiedSearchFragmentIT.kt
  3. 52 0
      src/androidTest/java/com/owncloud/android/ui/fragment/UnifiedSearchLocalRepository.kt
  4. 115 0
      src/androidTest/java/com/owncloud/android/ui/fragment/UnifiedSearchTestViewModel.kt
  5. 9 1
      src/main/java/com/nextcloud/client/di/ComponentsModule.java
  6. 7 0
      src/main/java/com/nextcloud/client/di/ViewModelModule.kt
  7. 11 1
      src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java
  8. 37 0
      src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchHeaderViewHolder.kt
  9. 36 0
      src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchItemViewHolder.kt
  10. 1083 0
      src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchListAdapter.java
  11. 16 5
      src/main/java/com/owncloud/android/ui/fragment/ExtendedListFragment.java
  12. 123 0
      src/main/java/com/owncloud/android/ui/fragment/UnifiedSearchFragment.kt
  13. 29 0
      src/main/java/com/owncloud/android/ui/unifiedsearch/IUnifiedSearchRepository.kt
  14. 49 0
      src/main/java/com/owncloud/android/ui/unifiedsearch/SearchOnProviderTask.kt
  15. 48 0
      src/main/java/com/owncloud/android/ui/unifiedsearch/UnifiedSearchRemoteRepository.kt
  16. 155 0
      src/main/java/com/owncloud/android/ui/unifiedsearch/UnifiedSearchViewModel.kt
  17. 3 1
      src/main/res/layout/list_fragment.xml
  18. 40 0
      src/main/res/layout/unified_search_header.xml
  19. 119 0
      src/main/res/layout/unified_search_item.xml
  20. 1 0
      src/main/res/values/strings.xml

+ 1 - 1
settings.gradle

@@ -1,3 +1,3 @@
 rootProject.name = 'Nextcloud'
 
-include ':'
+include ':nextcloud-android-library'

+ 88 - 0
src/androidTest/java/com/owncloud/android/ui/fragment/UnifiedSearchFragmentIT.kt

@@ -0,0 +1,88 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2020 Tobias Kaminsky
+ * Copyright (C) 2020 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 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 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 <https://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.ui.fragment
+
+import androidx.test.espresso.intent.rule.IntentsTestRule
+import androidx.test.internal.runner.junit4.statement.UiThreadStatement
+import com.nextcloud.client.TestActivity
+import com.owncloud.android.AbstractIT
+import com.owncloud.android.lib.common.SearchResult
+import com.owncloud.android.ui.unifiedsearch.UnifiedSearchViewModel
+import org.junit.Rule
+import org.junit.Test
+
+class UnifiedSearchFragmentIT : AbstractIT() {
+    @get:Rule
+    val testActivityRule = IntentsTestRule(TestActivity::class.java, true, false)
+
+    @Test
+    fun showSearchResult() {
+        val activity = testActivityRule.launchActivity(null)
+        val sut = UnifiedSearchFragment.newInstance(null)
+
+        activity.addFragment(sut)
+
+        shortSleep()
+
+        UiThreadStatement.runOnUiThread {
+            sut.onSearchResultChanged(
+                mutableListOf(
+                    SearchResult(
+                        "files",
+                        false,
+                        listOf(ex
+                            SearchResultEntry ("thumbnailUrl",
+                            "Test",
+                            "in /Files/",
+                            "resourceUrl",
+                            "icon",
+                            false
+                        )
+                    )
+                )
+            )
+            )
+        }
+
+        longSleep()
+    }
+
+    @Test
+    fun search() {
+        val activity = testActivityRule.launchActivity(null)
+        val sut = UnifiedSearchFragment.newInstance(null)
+        val testViewModel = UnifiedSearchViewModel()
+        val localRepository = UnifiedSearchLocalRepository()
+        testViewModel.setRepository(localRepository)
+
+        activity.addFragment(sut)
+
+        shortSleep()
+
+        UiThreadStatement.runOnUiThread {
+            sut.setViewModel(testViewModel)
+            sut.vm.startLoading("test")
+        }
+
+        longSleep()
+    }
+}

+ 52 - 0
src/androidTest/java/com/owncloud/android/ui/fragment/UnifiedSearchLocalRepository.kt

@@ -0,0 +1,52 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2020 Tobias Kaminsky
+ * Copyright (C) 2020 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 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 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 <https://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.ui.fragment
+
+import com.owncloud.android.lib.common.SearchResult
+import com.owncloud.android.lib.common.SearchResultEntry
+import com.owncloud.android.lib.common.utils.Log_OC
+import com.owncloud.android.ui.unifiedsearch.IUnifiedSearchRepository
+import com.owncloud.android.ui.unifiedsearch.SearchOnProviderTask
+import com.owncloud.android.ui.unifiedsearch.UnifiedSearchViewModel
+
+class UnifiedSearchLocalRepository : IUnifiedSearchRepository {
+    override fun refresh() {
+        TODO("Not yet implemented")
+    }
+
+    override fun startLoading() {
+        TODO("Not yet implemented")
+    }
+
+    override fun loadMore(query: String, vm: UnifiedSearchViewModel) {
+        val result = SearchOnProviderTask.Result(true, SearchResult("files", false, listOf(SearchResultEntry
+        ("thumbnailUrl",
+            "Test",
+            "in Files",
+            "resourceUrl",
+            "icon",
+            false))))
+        vm.onSearchResult(result)
+        Log_OC.d(this, "loadMore")
+    }
+}

+ 115 - 0
src/androidTest/java/com/owncloud/android/ui/fragment/UnifiedSearchTestViewModel.kt

@@ -0,0 +1,115 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 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.owncloud.android.ui.fragment
+
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import com.owncloud.android.lib.common.utils.Log_OC
+import com.owncloud.android.lib.resources.activities.model.Activity
+
+class UnifiedSearchTestViewModel : ViewModel() {
+
+    val activities: MutableLiveData<List<Activity>> = MutableLiveData<List<Activity>>(emptyList())
+    val error: MutableLiveData<String> = MutableLiveData<String>("")
+    val query: MutableLiveData<String> = MutableLiveData()
+    private var loadingStarted: Boolean = false
+    private var last: Int = -1
+
+    fun refresh() {
+        last = -1
+        activities.value = emptyList()
+        loadMore()
+    }
+
+    fun startLoading(query: String) {
+        if (!loadingStarted) {
+            loadingStarted = true
+            this.query.value = query
+            loadMore()
+        }
+    }
+
+    fun loadMore() {
+        Log_OC.d(this, "loadMore")
+//        if (isLoading.value != true) {
+//            val client = clientFactory.createNextcloudClient(currentUser.user)
+//            val task = SearchOnProviderTask("test", "files", client)
+//            runner.postQuickTask(task, onResult = this::onSearchResult, onError = this::onError)
+//            isLoading.value = true
+//        }
+    }
+
+//    fun openFile(fileUrl: String) {
+//        if (isLoading.value == false) {
+//            (isLoading as MutableLiveData).value = true
+//            val user = currentUser.user
+//            val task = GetRemoteFileTask(
+//                context,
+//                fileUrl,
+//                clientFactory.create(currentUser.user),
+//                FileDataStorageManager(user.toPlatformAccount(), contentResolver),
+//                user
+//            )
+//            runner.postQuickTask(task, onResult = this::onFileRequestResult)
+//        }
+//    }
+
+    fun clearError() {
+        error.value = ""
+    }
+
+    private fun onError(error: Throwable) {
+        Log_OC.d("Unified Search", "Error: " + error.stackTrace)
+    }
+
+//    private fun onSearchResult(result: SearchOnProviderTask.Result) {
+//        isLoading.value = false
+//
+//        if (result.success) {
+//           // activities.value = result.searchResult.entries
+//        } else {
+//            error.value = resources.getString(R.string.search_error)
+//        }
+//
+//        Log_OC.d("Unified Search", "Success: " + result.success)
+//        Log_OC.d("Unified Search", "Size: " + result.searchResult.entries.size)
+//    }
+
+//    private fun onActivitiesRequestResult(result: GetActivityListTask.Result) {
+//        isLoading.value = false
+//        if (result.success) {
+//            val existingActivities = activities.value ?: emptyList()
+//            val newActivities = listOf(existingActivities, result.activities).flatten()
+//            last = result.last
+//            activities.value = newActivities
+//        } else {
+//            error.value = resources.getString(R.string.activities_error)
+//        }
+//    }
+
+//    private fun onFileRequestResult(result: GetRemoteFileTask.Result) {
+//        (isLoading as MutableLiveData).value = false
+//        if (result.success) {
+//            (file as MutableLiveData).value = result.file
+//        } else {
+//            (file as MutableLiveData).value = null
+//        }
+//    }
+}

+ 9 - 1
src/main/java/com/nextcloud/client/di/ComponentsModule.java

@@ -78,6 +78,8 @@ import com.owncloud.android.ui.fragment.FileDetailSharingFragment;
 import com.owncloud.android.ui.fragment.GalleryFragment;
 import com.owncloud.android.ui.fragment.LocalFileListFragment;
 import com.owncloud.android.ui.fragment.OCFileListFragment;
+import com.owncloud.android.ui.fragment.PhotoFragment;
+import com.owncloud.android.ui.fragment.UnifiedSearchFragment;
 import com.owncloud.android.ui.fragment.contactsbackup.BackupFragment;
 import com.owncloud.android.ui.fragment.contactsbackup.BackupListFragment;
 import com.owncloud.android.ui.preview.PreviewImageActivity;
@@ -175,8 +177,14 @@ abstract class ComponentsModule {
     @ContributesAndroidInjector
     abstract SetStatusDialogFragment setStatusDialogFragment();
 
+    @ContributesAndroidInjector abstract PreviewTextFileFragment previewTextFileFragment();
+    @ContributesAndroidInjector abstract PreviewTextStringFragment previewTextStringFragment();
+
+    @ContributesAndroidInjector
+    abstract PhotoFragment photoFragment();
+
     @ContributesAndroidInjector
-    abstract PreviewTextFileFragment previewTextFileFragment();
+    abstract UnifiedSearchFragment searchFragment();
 
     @ContributesAndroidInjector
     abstract PreviewTextStringFragment previewTextStringFragment();

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

@@ -23,6 +23,8 @@ import androidx.lifecycle.ViewModel
 import androidx.lifecycle.ViewModelProvider
 import com.nextcloud.client.etm.EtmViewModel
 import com.nextcloud.client.logger.ui.LogsViewModel
+import com.owncloud.android.ui.activities.ActivitiesViewModel
+import com.owncloud.android.ui.unifiedsearch.UnifiedSearchViewModel
 import dagger.Binds
 import dagger.Module
 import dagger.multibindings.IntoMap
@@ -39,6 +41,11 @@ abstract class ViewModelModule {
     @ViewModelKey(LogsViewModel::class)
     abstract fun logsViewModel(vm: LogsViewModel): ViewModel
 
+    @Binds
+    @IntoMap
+    @ViewModelKey(UnifiedSearchViewModel::class)
+    abstract fun unifiedSearchViewModel(vm: UnifiedSearchViewModel): ViewModel
+
     @Binds
     abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
 }

+ 11 - 1
src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java

@@ -100,6 +100,7 @@ import com.owncloud.android.ui.fragment.FileFragment;
 import com.owncloud.android.ui.fragment.GalleryFragment;
 import com.owncloud.android.ui.fragment.OCFileListFragment;
 import com.owncloud.android.ui.fragment.TaskRetainerFragment;
+import com.owncloud.android.ui.fragment.UnifiedSearchFragment;
 import com.owncloud.android.ui.helpers.FileOperationsHelper;
 import com.owncloud.android.ui.helpers.UriUploader;
 import com.owncloud.android.ui.preview.PreviewImageActivity;
@@ -131,10 +132,10 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 
-import javax.annotation.Nullable;
 import javax.inject.Inject;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.appcompat.app.AlertDialog;
 import androidx.appcompat.widget.SearchView;
 import androidx.coordinatorlayout.widget.CoordinatorLayout;
@@ -2513,4 +2514,13 @@ public class FileDisplayActivity extends FileActivity
         fetchRemoteFileTask.execute();
 
     }
+
+    public void performUnifiedSearch(String query) {
+        FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
+        transaction.replace(R.id.left_fragment_container,
+                            UnifiedSearchFragment.Companion.newInstance(query),
+                            TAG_LIST_OF_FILES);
+        transaction.addToBackStack(null);
+        transaction.commit();
+    }
 }

+ 37 - 0
src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchHeaderViewHolder.kt

@@ -0,0 +1,37 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2020 Tobias Kaminsky
+ * Copyright (C) 2020 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 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 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 <https://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.ui.adapter
+
+import android.content.Context
+import com.afollestad.sectionedrecyclerview.SectionedViewHolder
+import com.owncloud.android.databinding.UnifiedSearchHeaderBinding
+import com.owncloud.android.lib.common.SearchResult
+import com.owncloud.android.utils.ThemeUtils
+
+class UnifiedSearchHeaderViewHolder(val binding: UnifiedSearchHeaderBinding, val context: Context) :
+    SectionedViewHolder(binding.root) {
+
+    fun bind(searchResult: SearchResult) {
+        binding.title.text = searchResult.name
+        binding.title.setTextColor(ThemeUtils.primaryColor(context))
+    }
+}

+ 36 - 0
src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchItemViewHolder.kt

@@ -0,0 +1,36 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2020 Tobias Kaminsky
+ * Copyright (C) 2020 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 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 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 <https://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.ui.adapter
+
+import android.content.Context
+import com.afollestad.sectionedrecyclerview.SectionedViewHolder
+import com.owncloud.android.databinding.UnifiedSearchItemBinding
+import com.owncloud.android.lib.common.SearchResultEntry
+
+class UnifiedSearchItemViewHolder(val binding: UnifiedSearchItemBinding, val context: Context) :
+    SectionedViewHolder(binding.root) {
+
+    fun bind(entry: SearchResultEntry) {
+        binding.title.text = entry.title
+        binding.subline.text = entry.subline
+    }
+}

+ 1083 - 0
src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchListAdapter.java

@@ -0,0 +1,1083 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * @author Chris Narkiewicz <hello@ezaquarii.com>
+ *
+ * Copyright (C) 2018 Tobias Kaminsky
+ * Copyright (C) 2018 Nextcloud
+ * Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
+ *
+ * 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.owncloud.android.ui.adapter;
+
+import android.accounts.AccountManager;
+import android.app.Activity;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Point;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.Filter;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import com.afollestad.sectionedrecyclerview.SectionedRecyclerViewAdapter;
+import com.afollestad.sectionedrecyclerview.SectionedViewHolder;
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.request.target.BitmapImageViewTarget;
+import com.elyeproj.loaderviewlibrary.LoaderImageView;
+import com.nextcloud.client.account.User;
+import com.nextcloud.client.account.UserAccountManager;
+import com.nextcloud.client.preferences.AppPreferences;
+import com.owncloud.android.MainApp;
+import com.owncloud.android.R;
+import com.owncloud.android.databinding.UnifiedSearchHeaderBinding;
+import com.owncloud.android.databinding.UnifiedSearchItemBinding;
+import com.owncloud.android.datamodel.FileDataStorageManager;
+import com.owncloud.android.datamodel.OCFile;
+import com.owncloud.android.datamodel.ThumbnailsCacheManager;
+import com.owncloud.android.datamodel.VirtualFolderType;
+import com.owncloud.android.db.ProviderMeta;
+import com.owncloud.android.lib.common.SearchResult;
+import com.owncloud.android.lib.common.SearchResultEntry;
+import com.owncloud.android.lib.common.operations.RemoteOperation;
+import com.owncloud.android.lib.common.operations.RemoteOperationResult;
+import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation;
+import com.owncloud.android.lib.resources.files.model.RemoteFile;
+import com.owncloud.android.lib.resources.shares.OCShare;
+import com.owncloud.android.lib.resources.shares.ShareType;
+import com.owncloud.android.operations.RefreshFolderOperation;
+import com.owncloud.android.operations.RemoteOperationFailedException;
+import com.owncloud.android.ui.TextDrawable;
+import com.owncloud.android.ui.fragment.ExtendedListFragment;
+import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface;
+import com.owncloud.android.utils.BitmapUtils;
+import com.owncloud.android.utils.DisplayUtils;
+import com.owncloud.android.utils.FileSortOrder;
+import com.owncloud.android.utils.FileStorageUtils;
+import com.owncloud.android.utils.MimeTypeUtil;
+import com.owncloud.android.utils.ThemeUtils;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.Vector;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.content.res.ResourcesCompat;
+import androidx.core.graphics.drawable.RoundedBitmapDrawable;
+import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
+import androidx.recyclerview.widget.RecyclerView;
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import kotlin.NotImplementedError;
+
+/**
+ * This Adapter populates a RecyclerView with all files and folders in a Nextcloud instance.
+ */
+public class UnifiedSearchListAdapter extends SectionedRecyclerViewAdapter<SectionedViewHolder>
+    implements DisplayUtils.AvatarGenerationListener {
+
+    private static final int showFilenameColumnThreshold = 4;
+    private final String userId;
+    private Activity activity;
+    private AppPreferences preferences;
+    private UserAccountManager accountManager;
+    private List<OCFile> mFiles = new ArrayList<>();
+    private List<OCFile> mFilesAll = new ArrayList<>();
+    private boolean hideItemOptions;
+    private long lastTimestamp;
+    private boolean gridView;
+    private boolean multiSelect;
+    private Set<OCFile> checkedFiles;
+
+    private List<SearchResult> list = new ArrayList<>();
+
+    private FileDataStorageManager mStorageManager;
+    private User user;
+    private OCFileListFragmentInterface ocFileListFragmentInterface;
+
+    private FilesFilter mFilesFilter;
+    private OCFile currentDirectory;
+    private static final String TAG = UnifiedSearchListAdapter.class.getSimpleName();
+
+    private static final int VIEWTYPE_FOOTER = 0;
+    private static final int VIEWTYPE_ITEM = 1;
+    private static final int VIEWTYPE_IMAGE = 2;
+    private static final int VIEWTYPE_HEADER = 3;
+
+    private List<ThumbnailsCacheManager.ThumbnailGenerationTask> asyncTasks = new ArrayList<>();
+    private boolean onlyOnDevice;
+    private boolean showShareAvatar = false;
+    private OCFile highlightedItem;
+    private Context context;
+
+    public UnifiedSearchListAdapter(Context context) {
+//        this.ocFileListFragmentInterface = ocFileListFragmentInterface;
+//        this.activity = activity;
+//        this.preferences = preferences;
+//        this.accountManager = accountManager;
+//        this.user = user;
+//        this.gridView = gridView;
+        checkedFiles = new HashSet<>();
+        this.context = context;
+
+        if (this.user != null) {
+            AccountManager platformAccountManager = AccountManager.get(this.activity);
+            userId = platformAccountManager.getUserData(this.user.toPlatformAccount(),
+                                                        com.owncloud.android.lib.common.accounts.AccountUtils.Constants.KEY_USER_ID);
+        } else {
+            userId = "";
+        }
+
+        // initialise thumbnails cache on background thread
+        new ThumbnailsCacheManager.InitDiskCacheTask().execute();
+    }
+
+    @NonNull
+    @Override
+    public SectionedViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+        if (viewType == VIEW_TYPE_HEADER) {
+            UnifiedSearchHeaderBinding binding = UnifiedSearchHeaderBinding.inflate(LayoutInflater.from(context),
+                                                                                    parent,
+                                                                                    false);
+
+            return new UnifiedSearchHeaderViewHolder(binding, context);
+        } else {
+            UnifiedSearchItemBinding binding = UnifiedSearchItemBinding.inflate(LayoutInflater.from(context),
+                                                                                parent,
+                                                                                false);
+
+            return new UnifiedSearchItemViewHolder(binding, context);
+        }
+    }
+
+    @Override
+    public int getSectionCount() {
+        return list.size();
+    }
+
+    @Override
+    public int getItemCount(int section) {
+        return list.get(section).getEntries().size();
+    }
+
+    @Override
+    public void onBindHeaderViewHolder(SectionedViewHolder holder, int section, boolean expanded) {
+        UnifiedSearchHeaderViewHolder headerViewHolder = (UnifiedSearchHeaderViewHolder) holder;
+
+        headerViewHolder.bind(list.get(section));
+    }
+
+    @Override
+    public void onBindFooterViewHolder(SectionedViewHolder holder, int section) {
+        throw new NotImplementedError();
+    }
+
+    @Override
+    public void onBindViewHolder(SectionedViewHolder holder, int section, int relativePosition, int absolutePosition) {
+        UnifiedSearchItemViewHolder itemViewHolder = (UnifiedSearchItemViewHolder) holder;
+        SearchResultEntry entry = list.get(section).getEntries().get(relativePosition);
+
+        itemViewHolder.bind(entry);
+    }
+
+    public boolean isMultiSelect() {
+        return multiSelect;
+    }
+
+    public void setMultiSelect(boolean bool) {
+        multiSelect = bool;
+        notifyDataSetChanged();
+    }
+
+    public boolean isCheckedFile(OCFile file) {
+        return checkedFiles.contains(file);
+    }
+
+    public void removeCheckedFile(OCFile file) {
+        checkedFiles.remove(file);
+    }
+
+    public void addCheckedFile(OCFile file) {
+        checkedFiles.add(file);
+        highlightedItem = null;
+    }
+
+    public void addAllFilesToCheckedFiles() {
+        checkedFiles.addAll(mFiles);
+    }
+
+    public void removeAllFilesFromCheckedFiles() {
+        checkedFiles.clear();
+    }
+
+    public int getItemPosition(OCFile file) {
+        int position = mFiles.indexOf(file);
+
+        if (shouldShowHeader()) {
+            position = position + 1;
+        }
+
+        return position;
+    }
+
+    public void setFavoriteAttributeForItemID(String fileId, boolean favorite) {
+        for (OCFile file : mFiles) {
+            if (file.getRemoteId().equals(fileId)) {
+                file.setFavorite(favorite);
+                break;
+            }
+        }
+
+        for (OCFile file : mFilesAll) {
+            if (file.getRemoteId().equals(fileId)) {
+                file.setFavorite(favorite);
+                break;
+            }
+        }
+
+        FileSortOrder sortOrder = preferences.getSortOrderByFolder(currentDirectory);
+        mFiles = sortOrder.sortCloudFiles(mFiles);
+
+        new Handler(Looper.getMainLooper()).post(this::notifyDataSetChanged);
+    }
+
+    public void refreshCommentsCount(String fileId) {
+        for (OCFile file : mFiles) {
+            if (file.getRemoteId().equals(fileId)) {
+                file.setUnreadCommentsCount(0);
+                break;
+            }
+        }
+
+        for (OCFile file : mFilesAll) {
+            if (file.getRemoteId().equals(fileId)) {
+                file.setUnreadCommentsCount(0);
+                break;
+            }
+        }
+
+        new Handler(Looper.getMainLooper()).post(this::notifyDataSetChanged);
+    }
+
+    public void setEncryptionAttributeForItemID(String fileId, boolean encrypted) {
+        int filesSize = mFiles.size();
+        for (int i = 0; i < filesSize; i++) {
+            if (mFiles.get(i).getRemoteId().equals(fileId)) {
+                OCFile file = mFiles.get(i);
+                file.setEncrypted(encrypted);
+                mStorageManager.saveFile(file);
+
+                break;
+            }
+        }
+
+        filesSize = mFilesAll.size();
+        for (int i = 0; i < filesSize; i++) {
+            if (mFilesAll.get(i).getRemoteId().equals(fileId)) {
+                mFilesAll.get(i).setEncrypted(encrypted);
+                break;
+            }
+        }
+
+        new Handler(Looper.getMainLooper()).post(this::notifyDataSetChanged);
+    }
+
+    @Override
+    public long getItemId(int position) {
+        if (mFiles == null || mFiles.size() <= position) {
+            return 0;
+        }
+        return mFiles.get(position).getFileId();
+    }
+
+
+    private void showFederatedShareAvatar(String user, float avatarRadius, Resources resources, ImageView avatar) {
+        // maybe federated share
+        String[] split = user.split("@");
+        String userId = split[0];
+        String server = split[1];
+
+        String url = "https://" + server + "/index.php/avatar/" + userId + "/" +
+            DisplayUtils.convertDpToPixel(avatarRadius, activity);
+
+        Drawable placeholder;
+        try {
+            placeholder = TextDrawable.createAvatarByUserId(userId, avatarRadius);
+        } catch (Exception e) {
+            Log_OC.e(TAG, "Error calculating RGB value for active account icon.", e);
+            placeholder = ThemeUtils.tintDrawable(ResourcesCompat.getDrawable(resources,
+                                                                              R.drawable.account_circle_white, null),
+                                                  R.color.black);
+        }
+
+        avatar.setTag(null);
+        Glide.with(activity).load(url)
+            .asBitmap()
+            .placeholder(placeholder)
+            .error(placeholder)
+            .into(new BitmapImageViewTarget(avatar) {
+                @Override
+                protected void setResource(Bitmap resource) {
+                    RoundedBitmapDrawable circularBitmapDrawable =
+                        RoundedBitmapDrawableFactory.create(activity.getResources(), resource);
+                    circularBitmapDrawable.setCircular(true);
+                    avatar.setImageDrawable(circularBitmapDrawable);
+                }
+            });
+    }
+
+    public static void setThumbnail(OCFile file,
+                                    ImageView thumbnailView,
+                                    User user,
+                                    FileDataStorageManager storageManager,
+                                    List<ThumbnailsCacheManager.ThumbnailGenerationTask> asyncTasks,
+                                    boolean gridView,
+                                    Context context) {
+        setThumbnail(file, thumbnailView, user, storageManager, asyncTasks, gridView, context, null, null);
+    }
+
+    private static void setThumbnail(OCFile file,
+                                     ImageView thumbnailView,
+                                     User user,
+                                     FileDataStorageManager storageManager,
+                                     List<ThumbnailsCacheManager.ThumbnailGenerationTask> asyncTasks,
+                                     boolean gridView,
+                                     Context context,
+                                     LoaderImageView shimmerThumbnail,
+                                     AppPreferences preferences) {
+        if (file.isFolder()) {
+            thumbnailView.setImageDrawable(MimeTypeUtil
+                                               .getFolderTypeIcon(file.isSharedWithMe() || file.isSharedWithSharee(),
+                                                                  file.isSharedViaLink(), file.isEncrypted(),
+                                                                  file.getMountType(), context));
+        } else {
+            if (file.getRemoteId() != null && file.isPreviewAvailable()) {
+                // Thumbnail in cache?
+                Bitmap thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache(
+                    ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.getRemoteId()
+                                                                                );
+
+                if (thumbnail != null && !file.isUpdateThumbnailNeeded()) {
+                    if (MimeTypeUtil.isVideo(file)) {
+                        Bitmap withOverlay = ThumbnailsCacheManager.addVideoOverlay(thumbnail);
+                        thumbnailView.setImageBitmap(withOverlay);
+                    } else {
+                        if (gridView) {
+                            BitmapUtils.setRoundedBitmapForGridMode(thumbnail, thumbnailView);
+                        } else {
+                            BitmapUtils.setRoundedBitmap(thumbnail, thumbnailView);
+                        }
+                    }
+                } else {
+                    // generate new thumbnail
+                    if (ThumbnailsCacheManager.cancelPotentialThumbnailWork(file, thumbnailView)) {
+                        try {
+                            final ThumbnailsCacheManager.ThumbnailGenerationTask task =
+                                new ThumbnailsCacheManager.ThumbnailGenerationTask(thumbnailView,
+                                                                                   storageManager,
+                                                                                   user.toPlatformAccount(),
+                                                                                   asyncTasks,
+                                                                                   gridView);
+                            if (thumbnail == null) {
+                                Drawable drawable = MimeTypeUtil.getFileTypeIcon(file.getMimeType(),
+                                                                                 file.getFileName(),
+                                                                                 user,
+                                                                                 context);
+                                if (drawable == null) {
+                                    drawable = ResourcesCompat.getDrawable(context.getResources(),
+                                                                           R.drawable.file_image,
+                                                                           null);
+                                }
+                                thumbnail = BitmapUtils.drawableToBitmap(drawable);
+                            }
+                            final ThumbnailsCacheManager.AsyncThumbnailDrawable asyncDrawable =
+                                new ThumbnailsCacheManager.AsyncThumbnailDrawable(context.getResources(),
+                                                                                  thumbnail, task);
+
+                            if (shimmerThumbnail != null && shimmerThumbnail.getVisibility() == View.GONE) {
+                                if (gridView) {
+                                    configShimmerGridImageSize(shimmerThumbnail, preferences.getGridColumns());
+                                }
+                                startShimmer(shimmerThumbnail, thumbnailView);
+                            }
+
+                            task.setListener(new ThumbnailsCacheManager.ThumbnailGenerationTask.Listener() {
+                                @Override
+                                public void onSuccess() {
+                                    stopShimmer(shimmerThumbnail, thumbnailView);
+                                }
+
+                                @Override
+                                public void onError() {
+                                    stopShimmer(shimmerThumbnail, thumbnailView);
+                                }
+                            });
+
+                            thumbnailView.setImageDrawable(asyncDrawable);
+                            asyncTasks.add(task);
+                            task.execute(new ThumbnailsCacheManager.ThumbnailGenerationTaskObject(file,
+                                                                                                  file.getRemoteId()));
+                        } catch (IllegalArgumentException e) {
+                            Log_OC.d(TAG, "ThumbnailGenerationTask : " + e.getMessage());
+                        }
+                    }
+                }
+
+                if ("image/png".equalsIgnoreCase(file.getMimeType())) {
+                    thumbnailView.setBackgroundColor(context.getResources().getColor(R.color.bg_default));
+                }
+            } else {
+                thumbnailView.setImageDrawable(MimeTypeUtil.getFileTypeIcon(file.getMimeType(),
+                                                                            file.getFileName(),
+                                                                            user,
+                                                                            context));
+            }
+        }
+    }
+
+//    @Override
+//    public void onViewAttachedToWindow(@NonNull RecyclerView.ViewHolder holder) {
+//        if (holder instanceof OCFileListGridImageViewHolder) {
+//            LoaderImageView thumbnailShimmer = ((OCFileListGridImageViewHolder) holder).shimmerThumbnail;
+//            if (thumbnailShimmer.getVisibility() == View.VISIBLE){
+//                thumbnailShimmer.setImageResource(R.drawable.background);
+//                thumbnailShimmer.resetLoader();
+//            }
+//        }
+//    }
+
+    private static Point getScreenSize(Context context) throws Exception {
+        final WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+        if (windowManager != null) {
+            final Point displaySize = new Point();
+            windowManager.getDefaultDisplay().getSize(displaySize);
+            return displaySize;
+        } else {
+            throw new Exception("WindowManager not found");
+        }
+    }
+
+    private static void configShimmerGridImageSize(LoaderImageView thumbnailShimmer, float gridColumns) {
+        FrameLayout.LayoutParams targetLayoutParams = (FrameLayout.LayoutParams) thumbnailShimmer.getLayoutParams();
+
+        try {
+            final Point screenSize = getScreenSize(thumbnailShimmer.getContext());
+            final int marginLeftAndRight = targetLayoutParams.leftMargin + targetLayoutParams.rightMargin;
+            final int size = Math.round(screenSize.x / gridColumns - marginLeftAndRight);
+
+            FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(size, size);
+            params.setMargins(targetLayoutParams.leftMargin,
+                              targetLayoutParams.topMargin,
+                              targetLayoutParams.rightMargin,
+                              targetLayoutParams.bottomMargin);
+            thumbnailShimmer.setLayoutParams(params);
+        } catch (Exception exception) {
+            Log_OC.e("ConfigShimmer", exception.getMessage());
+        }
+    }
+
+    private static void startShimmer(LoaderImageView thumbnailShimmer, ImageView thumbnailView) {
+        thumbnailShimmer.setImageResource(R.drawable.background);
+        thumbnailShimmer.resetLoader();
+        thumbnailView.setVisibility(View.GONE);
+        thumbnailShimmer.setVisibility(View.VISIBLE);
+    }
+
+    private static void stopShimmer(@Nullable LoaderImageView thumbnailShimmer, ImageView thumbnailView) {
+        if (thumbnailShimmer != null) {
+            thumbnailShimmer.setVisibility(View.GONE);
+            thumbnailView.setVisibility(View.VISIBLE);
+        }
+    }
+
+    private String getFooterText() {
+        int filesCount = 0;
+        int foldersCount = 0;
+        int count = mFiles.size();
+        OCFile file;
+        final boolean showHiddenFiles = preferences.isShowHiddenFilesEnabled();
+        for (int i = 0; i < count; i++) {
+            file = mFiles.get(i);
+            if (file.isFolder()) {
+                foldersCount++;
+            } else {
+                if (!file.isHidden() || showHiddenFiles) {
+                    filesCount++;
+                }
+            }
+        }
+
+
+        return generateFooterText(filesCount, foldersCount);
+    }
+
+    private String generateFooterText(int filesCount, int foldersCount) {
+        String output;
+        Resources resources = activity.getResources();
+
+        if (filesCount + foldersCount <= 0) {
+            output = "";
+        } else if (foldersCount <= 0) {
+            output = resources.getQuantityString(R.plurals.file_list__footer__file, filesCount, filesCount);
+        } else if (filesCount <= 0) {
+            output = resources.getQuantityString(R.plurals.file_list__footer__folder, foldersCount, foldersCount);
+        } else {
+            output = resources.getQuantityString(R.plurals.file_list__footer__file, filesCount, filesCount) + ", " +
+                resources.getQuantityString(R.plurals.file_list__footer__folder, foldersCount, foldersCount);
+        }
+
+        return output;
+    }
+
+    public OCFile getItem(int position) {
+        int newPosition = position;
+
+        if (shouldShowHeader() && position > 0) {
+            newPosition = position - 1;
+        }
+
+        return mFiles.get(newPosition);
+    }
+
+    public boolean shouldShowHeader() {
+        if (currentDirectory == null) {
+            return false;
+        }
+
+        if (MainApp.isOnlyOnDevice()) {
+            return false;
+        }
+
+        return !TextUtils.isEmpty(currentDirectory.getRichWorkspace());
+    }
+
+    private void showShareIcon(OCFileListGridImageViewHolder gridViewHolder, OCFile file) {
+        ImageView sharedIconView = gridViewHolder.shared;
+
+        if (gridViewHolder instanceof OCFileListItemViewHolder || file.getUnreadCommentsCount() == 0) {
+            sharedIconView.setVisibility(View.VISIBLE);
+
+            if (file.isSharedWithSharee() || file.isSharedWithMe()) {
+                if (showShareAvatar) {
+                    sharedIconView.setVisibility(View.GONE);
+                } else {
+                    sharedIconView.setVisibility(View.VISIBLE);
+                    sharedIconView.setImageResource(R.drawable.shared_via_users);
+                    sharedIconView.setContentDescription(activity.getString(R.string.shared_icon_shared));
+                }
+            } else if (file.isSharedViaLink()) {
+                sharedIconView.setImageResource(R.drawable.shared_via_link);
+                sharedIconView.setContentDescription(activity.getString(R.string.shared_icon_shared_via_link));
+            } else {
+                sharedIconView.setImageResource(R.drawable.ic_unshared);
+                sharedIconView.setContentDescription(activity.getString(R.string.shared_icon_share));
+            }
+            if (accountManager.accountOwnsFile(file, user.toPlatformAccount())) {
+                sharedIconView.setOnClickListener(view -> ocFileListFragmentInterface.onShareIconClick(file));
+            } else {
+                sharedIconView.setOnClickListener(view -> ocFileListFragmentInterface.showShareDetailView(file));
+            }
+        } else {
+            sharedIconView.setVisibility(View.GONE);
+        }
+    }
+
+    /**
+     * Change the adapted directory for a new one
+     *
+     * @param directory             New folder to adapt. Can be NULL, meaning "no content to adapt".
+     * @param updatedStorageManager Optional updated storage manager; used to replace
+     * @param limitToMimeType       show only files of this mimeType
+     */
+    public void swapDirectory(
+        User account,
+        OCFile directory,
+        FileDataStorageManager updatedStorageManager,
+        boolean onlyOnDevice, String limitToMimeType
+                             ) {
+        this.onlyOnDevice = onlyOnDevice;
+
+        if (updatedStorageManager != null && !updatedStorageManager.equals(mStorageManager)) {
+            mStorageManager = updatedStorageManager;
+            showShareAvatar = mStorageManager.getCapability(account.getAccountName()).getVersion().isShareesOnDavSupported();
+            this.user = account;
+        }
+        if (mStorageManager != null) {
+            mFiles = mStorageManager.getFolderContent(directory, onlyOnDevice);
+
+            if (!preferences.isShowHiddenFilesEnabled()) {
+                mFiles = filterHiddenFiles(mFiles);
+            }
+            if (!limitToMimeType.isEmpty()) {
+                mFiles = filterByMimeType(mFiles, limitToMimeType);
+            }
+            FileSortOrder sortOrder = preferences.getSortOrderByFolder(directory);
+            mFiles = sortOrder.sortCloudFiles(mFiles);
+            mFilesAll.clear();
+            mFilesAll.addAll(mFiles);
+
+            currentDirectory = directory;
+        } else {
+            mFiles.clear();
+            mFilesAll.clear();
+        }
+
+        notifyDataSetChanged();
+    }
+
+
+    public void setData(List<Object> objects,
+                        ExtendedListFragment.SearchType searchType,
+                        FileDataStorageManager storageManager,
+                        @Nullable OCFile folder,
+                        boolean clear) {
+        if (storageManager != null && mStorageManager == null) {
+            mStorageManager = storageManager;
+            showShareAvatar = mStorageManager.getCapability(user.getAccountName()).getVersion().isShareesOnDavSupported();
+        }
+
+        if (mStorageManager == null) {
+            mStorageManager = new FileDataStorageManager(user.toPlatformAccount(), activity.getContentResolver());
+        }
+
+        if (clear) {
+            mFiles.clear();
+            resetLastTimestamp();
+            preferences.setPhotoSearchTimestamp(0);
+
+            VirtualFolderType type;
+            switch (searchType) {
+                case FAVORITE_SEARCH:
+                    type = VirtualFolderType.FAVORITE;
+                    break;
+                case PHOTO_SEARCH:
+                    type = VirtualFolderType.PHOTOS;
+                    break;
+                default:
+                    type = VirtualFolderType.NONE;
+                    break;
+            }
+
+            mStorageManager.deleteVirtuals(type);
+        }
+
+        // early exit
+        if (objects.size() > 0 && mStorageManager != null) {
+            if (searchType == ExtendedListFragment.SearchType.SHARED_FILTER) {
+                parseShares(objects);
+            } else {
+                parseVirtuals(objects, searchType);
+            }
+        }
+
+        if (searchType != ExtendedListFragment.SearchType.PHOTO_SEARCH &&
+            searchType != ExtendedListFragment.SearchType.PHOTOS_SEARCH_FILTER &&
+            searchType != ExtendedListFragment.SearchType.RECENTLY_MODIFIED_SEARCH &&
+            searchType != ExtendedListFragment.SearchType.RECENTLY_MODIFIED_SEARCH_FILTER) {
+            FileSortOrder sortOrder = preferences.getSortOrderByFolder(folder);
+            mFiles = sortOrder.sortCloudFiles(mFiles);
+        } else {
+            mFiles = FileStorageUtils.sortOcFolderDescDateModifiedWithoutFavoritesFirst(mFiles);
+        }
+
+        mFilesAll.clear();
+        mFilesAll.addAll(mFiles);
+
+        new Handler(Looper.getMainLooper()).post(this::notifyDataSetChanged);
+    }
+
+    private void parseShares(List<Object> objects) {
+        List<OCShare> shares = new ArrayList<>();
+
+        for (Object shareObject : objects) {
+            // check type before cast as of long running data fetch it is possible that old result is filled
+            if (shareObject instanceof OCShare) {
+                OCShare ocShare = (OCShare) shareObject;
+
+                shares.add(ocShare);
+
+                // get ocFile from Server to have an up-to-date copy
+                RemoteOperationResult result = new ReadFileRemoteOperation(ocShare.getPath()).execute(user.toPlatformAccount(),
+                                                                                                      activity);
+
+                if (result.isSuccess()) {
+                    OCFile file = FileStorageUtils.fillOCFile((RemoteFile) result.getData().get(0));
+                    FileStorageUtils.searchForLocalFileInDefaultPath(file, user.toPlatformAccount());
+                    file = mStorageManager.saveFileWithParent(file, activity);
+
+                    ShareType newShareType = ocShare.getShareType();
+                    if (newShareType == ShareType.PUBLIC_LINK) {
+                        file.setSharedViaLink(true);
+                    } else if (newShareType == ShareType.USER ||
+                        newShareType == ShareType.GROUP ||
+                        newShareType == ShareType.EMAIL ||
+                        newShareType == ShareType.FEDERATED ||
+                        newShareType == ShareType.ROOM ||
+                        newShareType == ShareType.CIRCLE) {
+                        file.setSharedWithSharee(true);
+                    }
+
+                    mStorageManager.saveFile(file);
+
+                    if (!mFiles.contains(file)) {
+                        mFiles.add(file);
+                    }
+                } else {
+                    Log_OC.e(TAG, "Error in getting prop for file: " + ocShare.getPath());
+                }
+            }
+        }
+
+        mStorageManager.saveShares(shares);
+    }
+
+    private void parseVirtuals(List<Object> objects, ExtendedListFragment.SearchType searchType) {
+        VirtualFolderType type;
+        boolean onlyImages = false;
+
+        switch (searchType) {
+            case FAVORITE_SEARCH:
+                type = VirtualFolderType.FAVORITE;
+                break;
+            case PHOTO_SEARCH:
+                type = VirtualFolderType.PHOTOS;
+                onlyImages = true;
+
+                int lastPosition = objects.size() - 1;
+
+                if (lastPosition < 0) {
+                    lastTimestamp = -1;
+                    break;
+                }
+
+                RemoteFile lastFile = (RemoteFile) objects.get(lastPosition);
+                lastTimestamp = lastFile.getModifiedTimestamp() / 1000;
+                break;
+            default:
+                type = VirtualFolderType.NONE;
+                break;
+        }
+
+        List<ContentValues> contentValues = new ArrayList<>();
+
+        for (Object remoteFile : objects) {
+            OCFile ocFile = FileStorageUtils.fillOCFile((RemoteFile) remoteFile);
+            FileStorageUtils.searchForLocalFileInDefaultPath(ocFile, user.toPlatformAccount());
+
+            try {
+                if (ExtendedListFragment.SearchType.PHOTO_SEARCH == searchType) {
+                    mStorageManager.saveFile(ocFile);
+                } else {
+
+                    ocFile = mStorageManager.saveFileWithParent(ocFile, activity);
+
+                    // also sync folder content
+                    if (ocFile.isFolder()) {
+                        long currentSyncTime = System.currentTimeMillis();
+                        RemoteOperation refreshFolderOperation = new RefreshFolderOperation(ocFile,
+                                                                                            currentSyncTime,
+                                                                                            true,
+                                                                                            false,
+                                                                                            mStorageManager,
+                                                                                            user.toPlatformAccount(),
+                                                                                            activity);
+                        refreshFolderOperation.execute(user.toPlatformAccount(), activity);
+                    }
+                }
+
+                if (!onlyImages || MimeTypeUtil.isImage(ocFile)) {
+                    mFiles.add(ocFile);
+                }
+
+                ContentValues cv = new ContentValues();
+                cv.put(ProviderMeta.ProviderTableMeta.VIRTUAL_TYPE, type.toString());
+                cv.put(ProviderMeta.ProviderTableMeta.VIRTUAL_OCFILE_ID, ocFile.getFileId());
+
+                contentValues.add(cv);
+            } catch (RemoteOperationFailedException e) {
+                Log_OC.e(TAG, "Error saving file with parent" + e.getMessage(), e);
+            }
+        }
+
+        preferences.setPhotoSearchTimestamp(System.currentTimeMillis());
+        mStorageManager.saveVirtuals(contentValues);
+    }
+
+    public void showVirtuals(VirtualFolderType type, boolean onlyImages, FileDataStorageManager storageManager) {
+        mFiles = storageManager.getVirtualFolderContent(type, onlyImages);
+
+        if (VirtualFolderType.PHOTOS == type) {
+            mFiles = FileStorageUtils.sortOcFolderDescDateModifiedWithoutFavoritesFirst(mFiles);
+        }
+
+        mFilesAll.clear();
+        mFilesAll.addAll(mFiles);
+
+        new Handler(Looper.getMainLooper()).post(this::notifyDataSetChanged);
+    }
+
+
+    public void setSortOrder(@Nullable OCFile folder, FileSortOrder sortOrder) {
+        preferences.setSortOrder(folder, sortOrder);
+        mFiles = sortOrder.sortCloudFiles(mFiles);
+        notifyDataSetChanged();
+    }
+
+    public Set<OCFile> getCheckedItems() {
+        return checkedFiles;
+    }
+
+    public void setCheckedItem(Set<OCFile> files) {
+        checkedFiles.clear();
+        checkedFiles.addAll(files);
+    }
+
+    public void clearCheckedItems() {
+        checkedFiles.clear();
+    }
+
+    public List<OCFile> getFiles() {
+        return mFiles;
+    }
+
+    public Filter getFilter() {
+        if (mFilesFilter == null) {
+            mFilesFilter = new FilesFilter();
+        }
+        return mFilesFilter;
+    }
+
+    public void resetLastTimestamp() {
+        lastTimestamp = -1;
+    }
+
+    public long getLastTimestamp() {
+        return lastTimestamp;
+    }
+
+    @Override
+    public void avatarGenerated(Drawable avatarDrawable, Object callContext) {
+        ((ImageView) callContext).setImageDrawable(avatarDrawable);
+    }
+
+    @Override
+    public boolean shouldCallGeneratedCallback(String tag, Object callContext) {
+        return ((ImageView) callContext).getTag().equals(tag);
+    }
+
+    public void setHighlightedItem(OCFile highlightedItem) {
+        this.highlightedItem = highlightedItem;
+    }
+
+    private class FilesFilter extends Filter {
+        @Override
+        protected FilterResults performFiltering(CharSequence constraint) {
+            FilterResults results = new FilterResults();
+            Vector<OCFile> filteredFiles = new Vector<>();
+
+            if (!TextUtils.isEmpty(constraint)) {
+                for (OCFile file : mFilesAll) {
+                    if (file.getParentRemotePath().equals(currentDirectory.getRemotePath()) &&
+                        file.getFileName().toLowerCase(Locale.getDefault()).contains(
+                            constraint.toString().toLowerCase(Locale.getDefault())) &&
+                        !filteredFiles.contains(file)) {
+                        filteredFiles.add(file);
+                    }
+                }
+            }
+
+            results.values = filteredFiles;
+            results.count = filteredFiles.size();
+
+            return results;
+        }
+
+        @SuppressWarnings("unchecked")
+        @Override
+        protected void publishResults(CharSequence constraint, FilterResults results) {
+
+            Vector<OCFile> ocFiles = (Vector<OCFile>) results.values;
+            mFiles.clear();
+            if (ocFiles != null && ocFiles.size() > 0) {
+                mFiles.addAll(ocFiles);
+                if (!preferences.isShowHiddenFilesEnabled()) {
+                    mFiles = filterHiddenFiles(mFiles);
+                }
+                FileSortOrder sortOrder = preferences.getSortOrderByFolder(currentDirectory);
+                mFiles = sortOrder.sortCloudFiles(mFiles);
+            }
+
+            notifyDataSetChanged();
+        }
+    }
+
+    /**
+     * Filter for hidden files
+     *
+     * @param files Collection of files to filter
+     * @return Non-hidden files
+     */
+    private List<OCFile> filterHiddenFiles(List<OCFile> files) {
+        List<OCFile> ret = new ArrayList<>();
+
+        for (OCFile file : files) {
+            if (!file.isHidden() && !ret.contains(file)) {
+                ret.add(file);
+            }
+        }
+
+        return ret;
+    }
+
+    private List<OCFile> filterByMimeType(List<OCFile> files, String mimeType) {
+        List<OCFile> ret = new ArrayList<>();
+
+        for (OCFile file : files) {
+            if (file.isFolder() || file.getMimeType().startsWith(mimeType)) {
+                ret.add(file);
+            }
+        }
+
+        return ret;
+    }
+
+    public void cancelAllPendingTasks() {
+        for (ThumbnailsCacheManager.ThumbnailGenerationTask task : asyncTasks) {
+            if (task != null) {
+                task.cancel(true);
+                if (task.getGetMethod() != null) {
+                    Log_OC.d(TAG, "cancel: abort get method directly");
+                    task.getGetMethod().abort();
+                }
+            }
+        }
+
+        asyncTasks.clear();
+    }
+
+    public void setList(List<SearchResult> list) {
+        this.list = list;
+        notifyDataSetChanged();
+    }
+
+    public void setGridView(boolean bool) {
+        gridView = bool;
+    }
+
+    static class OCFileListItemViewHolder extends OCFileListGridItemViewHolder {
+        @BindView(R.id.file_size)
+        public TextView fileSize;
+
+        @BindView(R.id.last_mod)
+        public TextView lastModification;
+
+        @BindView(R.id.overflow_menu)
+        public ImageView overflowMenu;
+
+        @BindView(R.id.sharedAvatars)
+        public RelativeLayout sharedAvatars;
+
+        private OCFileListItemViewHolder(View itemView) {
+            super(itemView);
+            ButterKnife.bind(this, itemView);
+        }
+    }
+
+    static class OCFileListGridItemViewHolder extends OCFileListGridImageViewHolder {
+        @BindView(R.id.Filename) public TextView fileName;
+
+        private OCFileListGridItemViewHolder(View itemView) {
+            super(itemView);
+            ButterKnife.bind(this, itemView);
+        }
+    }
+
+    static class OCFileListGridImageViewHolder extends RecyclerView.ViewHolder {
+        @BindView(R.id.thumbnail)
+        public ImageView thumbnail;
+
+        @BindView(R.id.thumbnail_shimmer)
+        public LoaderImageView shimmerThumbnail;
+
+        @BindView(R.id.favorite_action)
+        public ImageView favorite;
+
+        @BindView(R.id.localFileIndicator)
+        public ImageView localFileIndicator;
+
+        @BindView(R.id.sharedIcon)
+        public ImageView shared;
+
+        @BindView(R.id.custom_checkbox)
+        public ImageView checkbox;
+
+        @BindView(R.id.ListItemLayout)
+        public View itemLayout;
+
+        @BindView(R.id.unreadComments)
+        public ImageView unreadComments;
+
+        private OCFileListGridImageViewHolder(View itemView) {
+            super(itemView);
+            ButterKnife.bind(this, itemView);
+            favorite.getDrawable().mutate();
+        }
+    }
+
+    static class OCFileListFooterViewHolder extends RecyclerView.ViewHolder {
+        @BindView(R.id.footerText)
+        public TextView footerText;
+
+        @BindView(R.id.loadingProgressBar)
+        public ProgressBar progressBar;
+
+        private OCFileListFooterViewHolder(View itemView) {
+            super(itemView);
+            ButterKnife.bind(this, itemView);
+        }
+    }
+
+    static class OCFileListHeaderViewHolder extends RecyclerView.ViewHolder {
+        @BindView(R.id.headerView)
+        public View headerView;
+
+        @BindView(R.id.headerText)
+        public TextView headerText;
+
+        private OCFileListHeaderViewHolder(View itemView) {
+            super(itemView);
+            ButterKnife.bind(this, itemView);
+        }
+    }
+}

+ 16 - 5
src/main/java/com/owncloud/android/ui/fragment/ExtendedListFragment.java

@@ -59,6 +59,7 @@ import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.lib.resources.files.SearchRemoteOperation;
+import com.owncloud.android.lib.resources.status.OwnCloudVersion;
 import com.owncloud.android.ui.EmptyRecyclerView;
 import com.owncloud.android.ui.activity.FileDisplayActivity;
 import com.owncloud.android.ui.activity.FolderPickerActivity;
@@ -220,9 +221,9 @@ public class ExtendedListFragment extends Fragment implements
             if (getActivity() != null && !(getActivity() instanceof FolderPickerActivity)
                 && !(getActivity() instanceof UploadFilesActivity)) {
                 if (getActivity() instanceof FileDisplayActivity) {
-                    OCFileListFragment fileFragment = ((FileDisplayActivity) getActivity()).getListOfFilesFragment();
-                    if (fileFragment != null) {
-                        fileFragment.setFabVisible(!hasFocus);
+                    Fragment fragment = ((FileDisplayActivity) getActivity()).getLeftFragment();
+                    if (fragment instanceof OCFileListFragment) {
+                        ((OCFileListFragment) fragment).setFabVisible(!hasFocus);
                     }
                 }
                 if (TextUtils.isEmpty(searchView.getQuery())) {
@@ -317,8 +318,18 @@ public class ExtendedListFragment extends Fragment implements
                 } else {
                     handler.post(() -> {
                         if (adapter instanceof OCFileListAdapter) {
-                            EventBus.getDefault().post(new SearchEvent(query,
-                                                                       SearchRemoteOperation.SearchType.FILE_SEARCH));
+                            if (accountManager
+                                .getUser()
+                                .getServer()
+                                .getVersion()
+                                .isNewerOrEqual(OwnCloudVersion.nextcloud_20)
+                            ) {
+                                ((FileDisplayActivity) activity).performUnifiedSearch(query);
+                            } else {
+                                EventBus.getDefault().post(
+                                    new SearchEvent(query, SearchRemoteOperation.SearchType.FILE_SEARCH)
+                                                          );
+                            }
                         } else if (adapter instanceof LocalFileListAdapter) {
                             LocalFileListAdapter localFileListAdapter = (LocalFileListAdapter) adapter;
                             localFileListAdapter.filter(query);

+ 123 - 0
src/main/java/com/owncloud/android/ui/fragment/UnifiedSearchFragment.kt

@@ -0,0 +1,123 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2020 Tobias Kaminsky
+ * Copyright (C) 2020 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.owncloud.android.ui.fragment
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.VisibleForTesting
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.ViewModelProvider
+import androidx.recyclerview.widget.GridLayoutManager
+import com.nextcloud.client.di.Injectable
+import com.nextcloud.client.di.ViewModelFactory
+import com.owncloud.android.databinding.ListFragmentBinding
+import com.owncloud.android.lib.common.SearchResult
+import com.owncloud.android.ui.adapter.UnifiedSearchListAdapter
+import com.owncloud.android.ui.unifiedsearch.UnifiedSearchViewModel
+import javax.inject.Inject
+
+/**
+ * Starts query to all capable unified search providers and displays them Opens result in our app, redirect to other
+ * apps, if installed, or opens browser
+ */
+class UnifiedSearchFragment : Fragment(), Injectable {
+    private lateinit var adapter: UnifiedSearchListAdapter
+    private var _binding: ListFragmentBinding? = null
+    private val binding get() = _binding!!
+    lateinit var vm: UnifiedSearchViewModel
+
+    @Inject
+    lateinit var vmFactory: ViewModelFactory
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        vm = ViewModelProvider(this, vmFactory).get(UnifiedSearchViewModel::class.java)
+        vm.searchResults.observe(this, this::onSearchResultChanged)
+
+        val query = savedInstanceState?.getString(ARG_QUERY).orEmpty()
+        if (query.isNotBlank()) {
+            vm.startLoading(query)
+            return
+        }
+
+        val queryArgument = arguments?.getString(ARG_QUERY).orEmpty()
+        if (queryArgument.isNotBlank()) {
+            vm.startLoading(queryArgument)
+        }
+    }
+
+    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+        _binding = ListFragmentBinding.inflate(inflater, container, false)
+
+        return binding.root;
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+
+        val gridLayoutManager = GridLayoutManager(requireContext(), 1)
+        adapter = UnifiedSearchListAdapter(requireContext())
+        adapter.setLayoutManager(gridLayoutManager)
+        binding.listRoot.layoutManager = gridLayoutManager
+        binding.listRoot.adapter = adapter
+    }
+
+    override fun onPause() {
+        super.onPause()
+        //photoSearchTask?.cancel(true)
+    }
+
+    override fun onDestroyView() {
+        super.onDestroyView()
+        _binding = null
+    }
+
+    @VisibleForTesting
+    fun onSearchResultChanged(list: MutableList<SearchResult>) {
+        binding.emptyList.emptyListView.visibility = View.GONE
+
+        adapter.setList(list)
+    }
+
+    @VisibleForTesting
+    fun setViewModel(testViewModel: UnifiedSearchViewModel) {
+        vm = testViewModel
+        vm.searchResults.observe(this, this::onSearchResultChanged)
+    }
+
+    companion object {
+        const val ARG_QUERY = "ARG_QUERY"
+        private const val MAX_ITEMS_PER_ROW = 10
+
+        /**
+         * Public factory method to get fragment.
+         */
+        fun newInstance(query: String?): UnifiedSearchFragment {
+            val fragment = UnifiedSearchFragment()
+            val args = Bundle()
+            args.putString(ARG_QUERY, query)
+            fragment.arguments = args
+            return fragment
+        }
+    }
+}

+ 29 - 0
src/main/java/com/owncloud/android/ui/unifiedsearch/IUnifiedSearchRepository.kt

@@ -0,0 +1,29 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2020 Tobias Kaminsky
+ * Copyright (C) 2020 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 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 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 <https://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.ui.unifiedsearch
+
+interface IUnifiedSearchRepository {
+    fun refresh()
+    fun startLoading()
+    fun loadMore(query: String, vm: UnifiedSearchViewModel)
+}

+ 49 - 0
src/main/java/com/owncloud/android/ui/unifiedsearch/SearchOnProviderTask.kt

@@ -0,0 +1,49 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 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.owncloud.android.ui.unifiedsearch
+
+import com.nextcloud.android.lib.resources.search.UnifiedSearchRemoteOperation
+import com.nextcloud.common.NextcloudClient
+import com.owncloud.android.lib.common.SearchResult
+import com.owncloud.android.lib.common.utils.Log_OC
+
+class SearchOnProviderTask(
+    private val query: String,
+    private val provider: String,
+    private val client: NextcloudClient
+) : () -> SearchOnProviderTask.Result {
+
+    data class Result(val success: Boolean = false, val searchResult: SearchResult = SearchResult())
+
+    override fun invoke(): Result {
+        Log_OC.d("Unified Search", "Run task")
+        val result = UnifiedSearchRemoteOperation(provider, query).execute(client)
+
+        Log_OC.d("Unified Search", "Task finished: " + result.isSuccess)
+        return if (result.isSuccess && result.singleData != null) {
+            Result(
+                success = true,
+                searchResult = result.singleData as SearchResult
+            )
+        } else {
+            Result()
+        }
+    }
+}

+ 48 - 0
src/main/java/com/owncloud/android/ui/unifiedsearch/UnifiedSearchRemoteRepository.kt

@@ -0,0 +1,48 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2020 Tobias Kaminsky
+ * Copyright (C) 2020 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 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 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 <https://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.ui.unifiedsearch
+
+import com.nextcloud.client.account.CurrentAccountProvider
+import com.nextcloud.client.core.AsyncRunner
+import com.nextcloud.client.network.ClientFactory
+import com.owncloud.android.lib.common.utils.Log_OC
+
+
+class UnifiedSearchRemoteRepository(private val clientFactory: ClientFactory,
+                                    private val currentAccountProvider: CurrentAccountProvider,
+                                    private val asyncRunner: AsyncRunner) : IUnifiedSearchRepository {
+    override fun refresh() {
+        TODO("Not yet implemented")
+    }
+
+    override fun startLoading() {
+        TODO("Not yet implemented")
+    }
+
+    override fun loadMore(query: String, vm: UnifiedSearchViewModel) {
+        Log_OC.d(this, "loadMore")
+        val client = clientFactory.createNextcloudClient(currentAccountProvider.user)
+        val task = SearchOnProviderTask(query, "files", client)
+        asyncRunner.postQuickTask(task, onResult = vm::onSearchResult, onError = vm::onError)
+    }
+}

+ 155 - 0
src/main/java/com/owncloud/android/ui/unifiedsearch/UnifiedSearchViewModel.kt

@@ -0,0 +1,155 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 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.owncloud.android.ui.unifiedsearch
+
+import android.content.res.Resources
+import androidx.annotation.VisibleForTesting
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import com.nextcloud.client.account.CurrentAccountProvider
+import com.nextcloud.client.core.AsyncRunner
+import com.nextcloud.client.network.ClientFactory
+import com.owncloud.android.R
+import com.owncloud.android.lib.common.SearchResult
+import com.owncloud.android.lib.common.utils.Log_OC
+import javax.inject.Inject
+
+@Suppress("LongParameterList")
+open // legacy code had large dependencies
+class UnifiedSearchViewModel() : ViewModel() {
+    lateinit var currentAccountProvider: CurrentAccountProvider
+    lateinit var runner: AsyncRunner
+    lateinit var clientFactory: ClientFactory
+    lateinit var resources: Resources
+
+    private lateinit var repository: IUnifiedSearchRepository
+    private var loadingStarted: Boolean = false
+    private var last: Int = -1
+
+    val isLoading: MutableLiveData<Boolean> = MutableLiveData<Boolean>(false)
+    val searchResults = MutableLiveData<MutableList<SearchResult>>(mutableListOf())
+    val error: MutableLiveData<String> = MutableLiveData<String>("")
+    val query: MutableLiveData<String> = MutableLiveData()
+
+    @Inject
+    constructor(
+        currentAccountProvider: CurrentAccountProvider,
+        runner: AsyncRunner,
+        clientFactory: ClientFactory,
+        resources: Resources) : this() {
+        this.currentAccountProvider = currentAccountProvider
+        this.runner = runner
+        this.clientFactory = clientFactory
+        this.resources = resources
+
+        repository = UnifiedSearchRemoteRepository(
+            clientFactory,
+            currentAccountProvider,
+            runner
+        )
+    }
+
+    open fun refresh() {
+        last = -1
+        searchResults.value = mutableListOf()
+        loadMore()
+    }
+
+    open fun startLoading(query: String) {
+        if (!loadingStarted) {
+            loadingStarted = true
+            this.query.value = query
+            loadMore()
+        }
+    }
+
+    open fun loadMore() {
+        val queryTerm = query.value.orEmpty()
+
+        if (isLoading.value != true && queryTerm.isNotBlank()) {
+            isLoading.value = true
+            repository.loadMore(queryTerm, this)
+        }
+    }
+
+
+//    fun openFile(fileUrl: String) {
+//        if (isLoading.value == false) {
+//            (isLoading as MutableLiveData).value = true
+//            val user = currentUser.user
+//            val task = GetRemoteFileTask(
+//                context,
+//                fileUrl,
+//                clientFactory.create(currentUser.user),
+//                FileDataStorageManager(user.toPlatformAccount(), contentResolver),
+//                user
+//            )
+//            runner.postQuickTask(task, onResult = this::onFileRequestResult)
+//        }
+//    }
+
+    open fun clearError() {
+        error.value = ""
+    }
+
+    fun onError(error: Throwable) {
+        Log_OC.d("Unified Search", "Error: " + error.stackTrace)
+    }
+
+    fun onSearchResult(result: SearchOnProviderTask.Result) {
+        isLoading.value = false
+
+        if (result.success) {
+            // TODO append if already exists
+            searchResults.value = mutableListOf(result.searchResult)
+        } else {
+            error.value = resources.getString(R.string.search_error)
+        }
+
+        Log_OC.d("Unified Search", "Success: " + result.success)
+        Log_OC.d("Unified Search", "Size: " + result.searchResult.entries.size)
+    }
+
+    @VisibleForTesting
+    fun setRepository(repository: IUnifiedSearchRepository) {
+        this.repository = repository
+    }
+
+//    private fun onActivitiesRequestResult(result: GetActivityListTask.Result) {
+//        isLoading.value = false
+//        if (result.success) {
+//            val existingActivities = activities.value ?: emptyList()
+//            val newActivities = listOf(existingActivities, result.activities).flatten()
+//            last = result.last
+//            activities.value = newActivities
+//        } else {
+//            error.value = resources.getString(R.string.activities_error)
+//        }
+//    }
+
+//    private fun onFileRequestResult(result: GetRemoteFileTask.Result) {
+//        (isLoading as MutableLiveData).value = false
+//        if (result.success) {
+//            (file as MutableLiveData).value = result.file
+//        } else {
+//            (file as MutableLiveData).value = null
+//        }
+//    }
+}

+ 3 - 1
src/main/res/layout/list_fragment.xml

@@ -36,6 +36,8 @@
             android:layout_height="match_parent" />
     </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
 
-    <include layout="@layout/empty_list" />
+    <include
+        android:id="@+id/empty_list"
+        layout="@layout/empty_list" />
 
 </RelativeLayout>

+ 40 - 0
src/main/res/layout/unified_search_header.xml

@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  Nextcloud Android client application
+
+  Copyright (C) 2018 Tobias Kaminsky
+  Copyright (C) 2018 Andy Scherzinger
+  Copyright (C) 2018 Nextcloud
+
+  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/>.
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/ListItemLayout"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal">
+
+    <TextView
+        android:id="@+id/title"
+        style="@style/TextAppearance.AppCompat.Body2"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:ellipsize="middle"
+        android:padding="@dimen/standard_half_padding"
+        android:textColor="@color/color_accent"
+        tools:text="Files" />
+</RelativeLayout>

+ 119 - 0
src/main/res/layout/unified_search_item.xml

@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  Nextcloud Android client application
+
+  Copyright (C) 2020 Tobias Kaminsky
+  Copyright (C) 2020 Nextcloud
+
+  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:tools="http://schemas.android.com/tools"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/upload_list_item_layout"
+    android:layout_width="match_parent"
+    android:layout_height="@dimen/standard_list_item_size"
+    android:baselineAligned="false"
+    android:orientation="horizontal">
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="@dimen/standard_list_item_size"
+        android:layout_height="@dimen/standard_list_item_size"
+        android:layout_marginStart="@dimen/zero"
+        android:layout_marginEnd="@dimen/standard_quarter_padding"
+        android:layout_marginBottom="@dimen/standard_padding">
+
+        <FrameLayout
+            android:id="@+id/thumbnail_layout"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent">
+
+            <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" />
+
+            <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>
+
+        <ImageView
+            android:id="@+id/localFileIndicator"
+            android:layout_width="@dimen/list_item_local_file_indicator_layout_width"
+            android:layout_height="@dimen/list_item_local_file_indicator_layout_height"
+            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" />
+
+        <ImageView
+            android:id="@+id/favorite_action"
+            android:layout_width="@dimen/list_item_favorite_action_layout_width"
+            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" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+    <LinearLayout
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_vertical"
+        android:layout_weight="1"
+        android:gravity="center_vertical"
+        android:orientation="vertical">
+
+        <TextView
+            android:id="@+id/title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical"
+            android:ellipsize="middle"
+            android:singleLine="true"
+            android:text=""
+            android:textColor="@color/text_color"
+            android:textSize="@dimen/two_line_primary_text_size"
+            tools:text="Test 123" />
+
+        <TextView
+            android:id="@+id/subline"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:ellipsize="middle"
+            android:singleLine="true"
+            android:textColor="@color/list_item_lastmod_and_filesize_text"
+            android:textSize="@dimen/two_line_secondary_text_size"
+            tools:text="in TestFolder" />
+
+    </LinearLayout>
+</LinearLayout>
+

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

@@ -990,4 +990,5 @@
     <string name="more">More</string>
     <string name="write_email">Send email</string>
     <string name="no_actions">No actions for this user</string>
+    <string name="search_error">Error getting search results</string>
 </resources>