Explorar o código

Optimize loading time for Shared view (#10015)

* Don't load entire OCFiles in the Shared view, as it takes too long

Instead, build partially-filled OCFiles from the OCShares we get

Signed-off-by: Álvaro Brey Vilas <alvaro.brey@nextcloud.com>

* Load metadata of shared files only when attempting to interact with them

This was quite a big undertaking and required a whole new fragment

Signed-off-by: Álvaro Brey Vilas <alvaro.brey@nextcloud.com>

* Fix wrong usages of deprecaed OCShare shareType setter

Signed-off-by: Álvaro Brey Vilas <alvaro.brey@nextcloud.com>
Álvaro Brey %!s(int64=3) %!d(string=hai) anos
pai
achega
5b8d11f738

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

@@ -79,6 +79,7 @@ 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.SharedListFragment;
 import com.owncloud.android.ui.fragment.UnifiedSearchFragment;
 import com.owncloud.android.ui.fragment.contactsbackup.BackupFragment;
 import com.owncloud.android.ui.fragment.contactsbackup.BackupListFragment;
@@ -215,4 +216,7 @@ abstract class ComponentsModule {
 
     @ContributesAndroidInjector
     abstract PreviewPdfFragment previewPDFFragment();
+
+    @ContributesAndroidInjector
+    abstract SharedListFragment sharedFragment();
 }

+ 12 - 0
app/src/main/java/com/owncloud/android/datamodel/OCFile.java

@@ -68,6 +68,7 @@ public class OCFile implements Parcelable, Comparable<OCFile>, ServerFileInterfa
      * synchronization of THE CONTENTS of this file.
      */
     private long modificationTimestampAtLastSyncForData;
+    private long firstShareTimestamp; // UNIX timestamp of the first share time
     private String remotePath;
     private String decryptedRemotePath;
     private String localPath;
@@ -160,6 +161,7 @@ public class OCFile implements Parcelable, Comparable<OCFile>, ServerFileInterfa
         mountType = (WebdavEntry.MountType) source.readSerializable();
         richWorkspace = source.readString();
         previewAvailable = source.readInt() == 1;
+        firstShareTimestamp = source.readLong();
     }
 
     @Override
@@ -193,6 +195,7 @@ public class OCFile implements Parcelable, Comparable<OCFile>, ServerFileInterfa
         dest.writeSerializable(mountType);
         dest.writeString(richWorkspace);
         dest.writeInt(previewAvailable ? 1 : 0);
+        dest.writeLong(firstShareTimestamp);
     }
 
     public void setDecryptedRemotePath(String path) {
@@ -455,6 +458,7 @@ public class OCFile implements Parcelable, Comparable<OCFile>, ServerFileInterfa
         encrypted = false;
         mountType = WebdavEntry.MountType.INTERNAL;
         richWorkspace = "";
+        firstShareTimestamp = 0;
     }
 
     /**
@@ -819,4 +823,12 @@ public class OCFile implements Parcelable, Comparable<OCFile>, ServerFileInterfa
     public void setRichWorkspace(String richWorkspace) {
         this.richWorkspace = richWorkspace;
     }
+
+    public long getFirstShareTimestamp() {
+        return firstShareTimestamp;
+    }
+
+    public void setFirstShareTimestamp(long firstShareTimestamp) {
+        this.firstShareTimestamp = firstShareTimestamp;
+    }
 }

+ 12 - 3
app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java

@@ -101,6 +101,7 @@ import com.owncloud.android.ui.events.SearchEvent;
 import com.owncloud.android.ui.fragment.FileDetailsSharingProcessFragment;
 import com.owncloud.android.ui.fragment.GalleryFragment;
 import com.owncloud.android.ui.fragment.OCFileListFragment;
+import com.owncloud.android.ui.fragment.SharedListFragment;
 import com.owncloud.android.ui.preview.PreviewTextStringFragment;
 import com.owncloud.android.ui.trashbin.TrashbinActivity;
 import com.owncloud.android.utils.BitmapUtils;
@@ -431,6 +432,7 @@ public abstract class DrawerActivity extends ToolbarActivity
         if (itemId == R.id.nav_all_files) {
             if (this instanceof FileDisplayActivity &&
                 !(((FileDisplayActivity) this).getLeftFragment() instanceof GalleryFragment) &&
+                !(((FileDisplayActivity) this).getLeftFragment() instanceof SharedListFragment) &&
                 !(((FileDisplayActivity) this).getLeftFragment() instanceof PreviewTextStringFragment)) {
                 showFiles(false);
                 ((FileDisplayActivity) this).browseToRoot();
@@ -470,8 +472,7 @@ public abstract class DrawerActivity extends ToolbarActivity
                 UserInfoActivity.openAccountRemovalConfirmationDialog(optionalUser.get(), getSupportFragmentManager());
             }
         } else if (itemId == R.id.nav_shared) {
-            handleSearchEvents(new SearchEvent("", SearchRemoteOperation.SearchType.SHARED_FILTER),
-                               menuItem.getItemId());
+            startSharedSearch(menuItem);
         } else if (itemId == R.id.nav_recently_modified) {
             handleSearchEvents(new SearchEvent("", SearchRemoteOperation.SearchType.RECENTLY_MODIFIED_SEARCH),
                                menuItem.getItemId());
@@ -519,6 +520,13 @@ public abstract class DrawerActivity extends ToolbarActivity
         }
     }
 
+    private void startSharedSearch(MenuItem menuItem) {
+        SearchEvent searchEvent = new SearchEvent("", SearchRemoteOperation.SearchType.SHARED_FILTER);
+        MainApp.showOnlyFilesOnDevice(false);
+
+        launchActivityForSearch(searchEvent, menuItem.getItemId());
+    }
+
     private void startPhotoSearch(MenuItem menuItem) {
         SearchEvent searchEvent = new SearchEvent("image/%", SearchRemoteOperation.SearchType.PHOTO_SEARCH);
         MainApp.showOnlyFilesOnDevice(false);
@@ -528,7 +536,8 @@ public abstract class DrawerActivity extends ToolbarActivity
 
     private void handleSearchEvents(SearchEvent searchEvent, int menuItemId) {
         if (this instanceof FileDisplayActivity) {
-            if (((FileDisplayActivity) this).getLeftFragment() instanceof GalleryFragment) {
+            final Fragment leftFragment = ((FileDisplayActivity) this).getLeftFragment();
+            if (leftFragment instanceof GalleryFragment || leftFragment instanceof SharedListFragment) {
                 launchActivityForSearch(searchEvent, menuItemId);
             } else {
                 EventBus.getDefault().post(searchEvent);

+ 11 - 1
app/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.SearchType;
+import com.owncloud.android.ui.fragment.SharedListFragment;
 import com.owncloud.android.ui.fragment.TaskRetainerFragment;
 import com.owncloud.android.ui.fragment.UnifiedSearchFragment;
 import com.owncloud.android.ui.helpers.FileOperationsHelper;
@@ -492,6 +493,13 @@ public class FileDisplayActivity extends FileActivity
                         bundle.putParcelable(OCFileListFragment.SEARCH_EVENT, searchEvent);
                         photoFragment.setArguments(bundle);
                         setLeftFragment(photoFragment);
+                    } else if (searchEvent.getSearchType().equals(SearchRemoteOperation.SearchType.SHARED_FILTER)) {
+                        Log_OC.d(this, "Switch to shared fragment");
+                        SharedListFragment sharedListFragment = new SharedListFragment();
+                        Bundle bundle = new Bundle();
+                        bundle.putParcelable(OCFileListFragment.SEARCH_EVENT, searchEvent);
+                        sharedListFragment.setArguments(bundle);
+                        setLeftFragment(sharedListFragment);
                     } else {
                         Log_OC.d(this, "Switch to oc file search fragment");
 
@@ -2287,8 +2295,10 @@ public class FileDisplayActivity extends FileActivity
     public void onMessageEvent(final SearchEvent event) {
         if (SearchRemoteOperation.SearchType.PHOTO_SEARCH == event.getSearchType()) {
             Log_OC.d(this, "Switch to photo search fragment");
-
             setLeftFragment(new GalleryFragment());
+        } else if (event.getSearchType() == SearchRemoteOperation.SearchType.SHARED_FILTER) {
+            Log_OC.d(this, "Switch to Shared fragment");
+            setLeftFragment(new SharedListFragment());
         }
     }
 

+ 0 - 6
app/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.java

@@ -27,7 +27,6 @@ import android.annotation.SuppressLint;
 import android.graphics.Bitmap;
 import android.graphics.Color;
 import android.graphics.drawable.Drawable;
-import android.os.Bundle;
 import android.view.View;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
@@ -73,11 +72,6 @@ public abstract class ToolbarActivity extends BaseActivity {
     protected AppCompatSpinner mToolbarSpinner;
     private boolean isHomeSearchToolbarShow = false;
 
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-    }
-
     /**
      * Toolbar setup that must be called in implementer's {@link #onCreate} after {@link #setContentView} if they want
      * to use the toolbar.

+ 52 - 50
app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java

@@ -63,9 +63,7 @@ import com.owncloud.android.db.ProviderMeta;
 import com.owncloud.android.files.services.FileDownloader;
 import com.owncloud.android.files.services.FileUploader;
 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;
@@ -148,7 +146,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
         OCFileListFragmentInterface ocFileListFragmentInterface,
         boolean argHideItemOptions,
         boolean gridView
-    ) {
+                            ) {
         this.ocFileListFragmentInterface = ocFileListFragmentInterface;
         this.activity = activity;
         this.preferences = preferences;
@@ -162,7 +160,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
         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);
+                                                        com.owncloud.android.lib.common.accounts.AccountUtils.Constants.KEY_USER_ID);
         } else {
             userId = "";
         }
@@ -219,7 +217,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
                 if (removeFromList) {
                     mFiles.remove(file);
                 }
-                
+
                 break;
             }
         }
@@ -386,10 +384,10 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
 
             if (highlightedItem != null && file.getFileId() == highlightedItem.getFileId()) {
                 gridViewHolder.getItemLayout().setBackgroundColor(activity.getResources()
-                                                                 .getColor(R.color.selected_item_background));
+                                                                      .getColor(R.color.selected_item_background));
             } else if (isCheckedFile(file)) {
                 gridViewHolder.getItemLayout().setBackgroundColor(activity.getResources()
-                                                                 .getColor(R.color.selected_item_background));
+                                                                      .getColor(R.color.selected_item_background));
                 gridViewHolder.getCheckbox().setImageDrawable(
                     ThemeDrawableUtils.tintDrawable(R.drawable.ic_checkbox_marked,
                                                     ThemeColorUtils.primaryColor(activity)));
@@ -403,7 +401,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
             if (!hideItemOptions) {
                 gridViewHolder.getItemLayout().setLongClickable(true);
                 gridViewHolder.getItemLayout().setOnLongClickListener(v ->
-                                                                     ocFileListFragmentInterface.onLongItemClicked(file));
+                                                                          ocFileListFragmentInterface.onLongItemClicked(file));
             }
 
             // unread comments
@@ -458,11 +456,34 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
                     }
 
                     itemViewHolder.getFileSize().setText(DisplayUtils.bytesToHumanReadable(localSize));
+                    itemViewHolder.getFileSize().setVisibility(View.VISIBLE);
+                    itemViewHolder.getFileSizeSeparator().setVisibility(View.VISIBLE);
+                } else {
+                    final long fileLength = file.getFileLength();
+                    if (fileLength >= 0) {
+                        itemViewHolder.getFileSize().setText(DisplayUtils.bytesToHumanReadable(fileLength));
+                        itemViewHolder.getFileSize().setVisibility(View.VISIBLE);
+                        itemViewHolder.getFileSizeSeparator().setVisibility(View.VISIBLE);
+                    } else {
+                        itemViewHolder.getFileSize().setVisibility(View.GONE);
+                        itemViewHolder.getFileSizeSeparator().setVisibility(View.GONE);
+                    }
+                }
+
+                final long modificationTimestamp = file.getModificationTimestamp();
+                if (modificationTimestamp > 0) {
+                    itemViewHolder.getLastModification().setText(DisplayUtils.getRelativeTimestamp(activity,
+                                                                                                   modificationTimestamp));
+                    itemViewHolder.getLastModification().setVisibility(View.VISIBLE);
+                } else if (file.getFirstShareTimestamp() > 0) {
+                    itemViewHolder.getLastModification().setText(
+                        DisplayUtils.getRelativeTimestamp(activity, file.getFirstShareTimestamp())
+                                                                );
+                    itemViewHolder.getLastModification().setVisibility(View.VISIBLE);
                 } else {
-                    itemViewHolder.getFileSize().setText(DisplayUtils.bytesToHumanReadable(file.getFileLength()));
+                    itemViewHolder.getLastModification().setVisibility(View.GONE);
                 }
-                itemViewHolder.getLastModification().setText(DisplayUtils.getRelativeTimestamp(activity,
-                                                                                          file.getModificationTimestamp()));
+
 
                 if (multiSelect || gridView || hideItemOptions) {
                     itemViewHolder.getOverflowMenu().setVisibility(View.GONE);
@@ -505,6 +526,9 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
                 }
 
                 gridViewHolder.getFavorite().setVisibility(file.isFavorite() ? View.VISIBLE : View.GONE);
+            } else {
+                gridViewHolder.getLocalFileIndicator().setVisibility(View.GONE);
+                gridViewHolder.getFavorite().setVisibility(View.GONE);
             }
 
             if (multiSelect) {
@@ -567,7 +591,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
                 // Thumbnail in cache?
                 Bitmap thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache(
                     ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.getRemoteId()
-                );
+                                                                                );
 
                 if (thumbnail != null && !file.isUpdateThumbnailNeeded()) {
                     stopShimmer(shimmerThumbnail, thumbnailView);
@@ -655,7 +679,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
     public void onViewAttachedToWindow(@NonNull RecyclerView.ViewHolder holder) {
         if (holder instanceof ListGridImageViewHolder) {
             LoaderImageView thumbnailShimmer = ((ListGridImageViewHolder) holder).getShimmerThumbnail();
-            if (thumbnailShimmer.getVisibility() == View.VISIBLE){
+            if (thumbnailShimmer.getVisibility() == View.VISIBLE) {
                 thumbnailShimmer.setImageResource(R.drawable.background);
                 thumbnailShimmer.resetLoader();
             }
@@ -834,7 +858,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
         OCFile directory,
         FileDataStorageManager updatedStorageManager,
         boolean onlyOnDevice, String limitToMimeType
-    ) {
+                             ) {
         this.onlyOnDevice = onlyOnDevice;
 
         if (updatedStorageManager != null && !updatedStorageManager.equals(mStorageManager)) {
@@ -913,12 +937,12 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
             }
         }
 
-        if (searchType != SearchType.GALLERY_SEARCH &&
-            searchType != SearchType.RECENTLY_MODIFIED_SEARCH) {
+        if (searchType == SearchType.GALLERY_SEARCH ||
+            searchType == SearchType.RECENTLY_MODIFIED_SEARCH) {
+            mFiles = FileStorageUtils.sortOcFolderDescDateModifiedWithoutFavoritesFirst(mFiles);
+        } else if (searchType != SearchType.SHARED_FILTER) {
             FileSortOrder sortOrder = preferences.getSortOrderByFolder(folder);
             mFiles = sortOrder.sortCloudFiles(mFiles);
-        } else {
-            mFiles = FileStorageUtils.sortOcFolderDescDateModifiedWithoutFavoritesFirst(mFiles);
         }
 
         mFilesAll.clear();
@@ -934,41 +958,12 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
             // 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.getAccountName());
-                    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());
-                }
             }
         }
 
+        List<OCFile> files = OCShareToOCFileConverter.buildOCFilesFromShares(shares);
+        mFiles.addAll(files);
         mStorageManager.saveShares(shares);
     }
 
@@ -1224,7 +1219,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
         currentDirectory = folder;
     }
 
-    static class OCFileListItemViewHolder extends RecyclerView.ViewHolder implements ListItemViewHolder{
+    static class OCFileListItemViewHolder extends RecyclerView.ViewHolder implements ListItemViewHolder {
         protected ListItemBinding binding;
 
         private OCFileListItemViewHolder(ListItemBinding binding) {
@@ -1238,6 +1233,11 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
             return binding.fileSize;
         }
 
+        @Override
+        public View getFileSizeSeparator() {
+            return binding.fileSeparator;
+        }
+
         @Override
         public TextView getLastModification() {
             return binding.lastMod;
@@ -1447,6 +1447,8 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
     interface ListItemViewHolder extends ListGridItemViewHolder {
         TextView getFileSize();
 
+        View getFileSizeSeparator();
+
         TextView getLastModification();
 
         ImageView getOverflowMenu();

+ 77 - 0
app/src/main/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverter.kt

@@ -0,0 +1,77 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Álvaro Brey Vilas
+ * Copyright (C) 2022 Álvaro Brey Vilas
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.ui.adapter
+
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.lib.resources.shares.OCShare
+import com.owncloud.android.lib.resources.shares.ShareType
+import com.owncloud.android.lib.resources.shares.ShareeUser
+
+object OCShareToOCFileConverter {
+    private const val MILLIS_PER_SECOND = 1000
+
+    /**
+     * Generates a list of incomplete [OCFile] from a list of [OCShare]
+     *
+     * This is actually pretty complex as we get one [OCShare] item for each shared instance for the same folder
+     *
+     * **THIS ONLY WORKS WITH FILES SHARED *BY* THE USER, NOT FOR SHARES *WITH* THE USER**
+     */
+    @JvmStatic
+    fun buildOCFilesFromShares(shares: List<OCShare>): List<OCFile> {
+        val groupedByPath: Map<String, List<OCShare>> = shares.groupBy { it.path }
+        return groupedByPath
+            .map { (path: String, shares: List<OCShare>) -> buildOcFile(path, shares) }
+            .sortedByDescending { it.firstShareTimestamp }
+    }
+
+    private fun buildOcFile(path: String, shares: List<OCShare>): OCFile {
+        require(shares.all { it.path == path })
+        // common attributes
+        val firstShare = shares.first()
+        val file = OCFile(path).apply {
+            decryptedRemotePath = path
+            ownerId = firstShare.userId
+            ownerDisplayName = firstShare.ownerDisplayName
+            isPreviewAvailable = firstShare.isHasPreview
+            mimeType = firstShare.mimetype
+            note = firstShare.note
+            fileId = firstShare.fileSource
+            remoteId = firstShare.remoteId.toString()
+            // use first share timestamp as timestamp
+            firstShareTimestamp = shares.minOf { it.sharedDate * MILLIS_PER_SECOND }
+            // don't have file length or mod timestamp
+            fileLength = -1
+            modificationTimestamp = -1
+        }
+        if (shares.any { it.shareType in listOf(ShareType.PUBLIC_LINK, ShareType.EMAIL) }) {
+            file.isSharedViaLink = true
+        }
+        if (shares.any { it.shareType !in listOf(ShareType.PUBLIC_LINK, ShareType.EMAIL) }) {
+            file.isSharedWithSharee = true
+            file.sharees = shares
+                .filter { it.shareType != ShareType.PUBLIC_LINK && it.shareType != ShareType.EMAIL }
+                .map { ShareeUser(it.shareWith, it.sharedWithDisplayName, it.shareType) }
+        }
+        return file
+    }
+}

+ 3 - 1
app/src/main/java/com/owncloud/android/ui/adapter/ShareeListAdapter.java

@@ -196,7 +196,9 @@ public class ShareeListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
         shares.addAll(users);
 
         // add internal share link at end
-        shares.add(new OCShare().setShareType(ShareType.INTERNAL));
+        final OCShare ocShare = new OCShare();
+        ocShare.setShareType(ShareType.INTERNAL);
+        shares.add(ocShare);
     }
 
     public List<OCShare> getShares() {

+ 3 - 1
app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java

@@ -410,7 +410,9 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
 
 
         if (publicShares.isEmpty() && containsNoNewPublicShare(adapter.getShares())) {
-            publicShares.add(new OCShare().setShareType(ShareType.NEW_PUBLIC_LINK));
+            final OCShare ocShare = new OCShare();
+            ocShare.setShareType(ShareType.NEW_PUBLIC_LINK);
+            publicShares.add(ocShare);
         } else {
             adapter.removeNewPublicShare();
         }

+ 18 - 26
app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java

@@ -71,7 +71,6 @@ import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.lib.resources.e2ee.ToggleEncryptionRemoteOperation;
 import com.owncloud.android.lib.resources.files.SearchRemoteOperation;
 import com.owncloud.android.lib.resources.files.ToggleFavoriteRemoteOperation;
-import com.owncloud.android.lib.resources.shares.GetSharesRemoteOperation;
 import com.owncloud.android.lib.resources.status.OCCapability;
 import com.owncloud.android.ui.activity.FileActivity;
 import com.owncloud.android.ui.activity.FileDisplayActivity;
@@ -1509,7 +1508,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
         handleSearchEvent(event);
     }
 
-    private void handleSearchEvent(SearchEvent event) {
+    protected void handleSearchEvent(SearchEvent event) {
         if (SearchRemoteOperation.SearchType.PHOTO_SEARCH == event.getSearchType()) {
             return;
         }
@@ -1537,28 +1536,27 @@ public class OCFileListFragment extends ExtendedListFragment implements
 
         final User currentUser = accountManager.getUser();
 
-        final RemoteOperation remoteOperation;
-        if (currentSearchType != SearchType.SHARED_FILTER) {
-            boolean searchOnlyFolders = false;
-            if (getArguments() != null && getArguments().getBoolean(ARG_SEARCH_ONLY_FOLDER, false)) {
-                searchOnlyFolders = true;
-            }
+        final RemoteOperation remoteOperation = getSearchRemoteOperation(currentUser, event);
 
-            OCCapability ocCapability = mContainerActivity.getStorageManager()
-                .getCapability(currentUser.getAccountName());
+        remoteOperationAsyncTask = new OCFileListSearchAsyncTask(mContainerActivity, this, remoteOperation, currentUser, event);
 
-            remoteOperation = new SearchRemoteOperation(event.getSearchQuery(),
-                                                        event.getSearchType(),
-                                                        searchOnlyFolders,
-                                                        ocCapability);
-        } else {
-            remoteOperation = new GetSharesRemoteOperation();
-        }
+        remoteOperationAsyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+    }
 
 
-        remoteOperationAsyncTask = new OCFileListSearchAsyncTask(mContainerActivity, this, remoteOperation, currentUser, event);
+    protected RemoteOperation getSearchRemoteOperation(final User currentUser, final SearchEvent event) {
+        boolean searchOnlyFolders = false;
+        if (getArguments() != null && getArguments().getBoolean(ARG_SEARCH_ONLY_FOLDER, false)) {
+            searchOnlyFolders = true;
+        }
 
-        remoteOperationAsyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+        OCCapability ocCapability = mContainerActivity.getStorageManager()
+            .getCapability(currentUser.getAccountName());
+
+        return new SearchRemoteOperation(event.getSearchQuery(),
+                                         event.getSearchType(),
+                                         searchOnlyFolders,
+                                         ocCapability);
     }
 
     @Subscribe(threadMode = ThreadMode.BACKGROUND)
@@ -1609,12 +1607,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
     }
 
     protected void setTitle(@StringRes final int title) {
-        getActivity().runOnUiThread(() -> {
-            if (getActivity() != null && ((FileDisplayActivity) getActivity()).getSupportActionBar() != null) {
-                ThemeToolbarUtils.setColoredTitle(((FileDisplayActivity) getActivity()).getSupportActionBar(),
-                                           title, getContext());
-            }
-        });
+        setTitle(getContext().getString(title));
     }
 
     protected void setTitle(final String title) {
@@ -1698,7 +1691,6 @@ public class OCFileListFragment extends ExtendedListFragment implements
         }
         SearchRemoteOperation.SearchType searchType = event.getSearchType();
         return !TextUtils.isEmpty(event.getSearchQuery()) ||
-            searchType == SearchRemoteOperation.SearchType.SHARED_SEARCH ||
             searchType == SearchRemoteOperation.SearchType.SHARED_FILTER ||
             searchType == SearchRemoteOperation.SearchType.FAVORITE_SEARCH ||
             searchType == SearchRemoteOperation.SearchType.RECENTLY_MODIFIED_SEARCH;

+ 162 - 0
app/src/main/java/com/owncloud/android/ui/fragment/SharedListFragment.kt

@@ -0,0 +1,162 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2019 Tobias Kaminsky
+ * Copyright (C) 2019 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.os.Handler
+import android.view.View
+import androidx.lifecycle.lifecycleScope
+import com.nextcloud.client.account.User
+import com.nextcloud.client.di.Injectable
+import com.nextcloud.client.logger.Logger
+import com.owncloud.android.R
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.lib.common.operations.RemoteOperation
+import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation
+import com.owncloud.android.lib.resources.files.SearchRemoteOperation
+import com.owncloud.android.lib.resources.files.model.RemoteFile
+import com.owncloud.android.lib.resources.shares.GetSharesRemoteOperation
+import com.owncloud.android.ui.activity.FileDisplayActivity
+import com.owncloud.android.ui.events.SearchEvent
+import com.owncloud.android.utils.DisplayUtils
+import com.owncloud.android.utils.FileStorageUtils
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import javax.inject.Inject
+
+/**
+ * A Fragment that lists folders shared by the user
+ */
+@Suppress("TooManyFunctions")
+class SharedListFragment : OCFileListFragment(), Injectable {
+
+    @Inject
+    lateinit var logger: Logger
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        searchFragment = true
+    }
+
+    override fun onActivityCreated(savedInstanceState: Bundle?) {
+        super.onActivityCreated(savedInstanceState)
+        mAdapter.setShowMetadata(false)
+        currentSearchType = SearchType.SHARED_FILTER
+        searchEvent = SearchEvent("", SearchRemoteOperation.SearchType.SHARED_FILTER)
+        menuItemAddRemoveValue = MenuItemAddRemove.REMOVE_GRID_AND_SORT
+        requireActivity().invalidateOptionsMenu()
+    }
+
+    override fun onResume() {
+        super.onResume()
+        Handler().post {
+            if (activity is FileDisplayActivity) {
+                val fileDisplayActivity = activity as FileDisplayActivity
+                fileDisplayActivity.updateActionBarTitleAndHomeButtonByString(getString(R.string.drawer_item_shared))
+                fileDisplayActivity.setMainFabVisible(false)
+            }
+        }
+    }
+
+    override fun getSearchRemoteOperation(currentUser: User?, event: SearchEvent?): RemoteOperation<*> {
+        return GetSharesRemoteOperation()
+    }
+
+    private suspend fun fetchFileData(partialFile: OCFile): OCFile? {
+        return withContext(Dispatchers.IO) {
+            val user = accountManager.user
+            val fetchResult = ReadFileRemoteOperation(partialFile.remotePath)
+                .execute(user.toPlatformAccount(), context)
+            if (!fetchResult.isSuccess) {
+                logger.e(SHARED_TAG, "Error fetching file")
+                if (fetchResult.isException) {
+                    logger.e(SHARED_TAG, "exception: ", fetchResult.exception)
+                }
+                null
+            } else {
+                val remoteFile = fetchResult.data[0] as RemoteFile
+                val file = FileStorageUtils.fillOCFile(remoteFile)
+                FileStorageUtils.searchForLocalFileInDefaultPath(file, user.accountName)
+                val savedFile = mContainerActivity.storageManager.saveFileWithParent(file, context)
+                savedFile.apply {
+                    isSharedViaLink = partialFile.isSharedViaLink
+                    isSharedWithSharee = partialFile.isSharedWithSharee
+                    sharees = partialFile.sharees
+                }
+            }
+        }
+    }
+
+    private fun fetchFileAndRun(partialFile: OCFile, block: (file: OCFile) -> Unit) {
+        lifecycleScope.launch {
+            isLoading = true
+            val file = fetchFileData(partialFile)
+            isLoading = false
+            if (file != null) {
+                block(file)
+            } else {
+                DisplayUtils.showSnackMessage(requireActivity(), R.string.error_retrieving_file)
+            }
+        }
+    }
+
+    override fun onShareIconClick(file: OCFile) {
+        fetchFileAndRun(file) { fetched ->
+            super.onShareIconClick(fetched)
+        }
+    }
+
+    override fun showShareDetailView(file: OCFile) {
+        fetchFileAndRun(file) { fetched ->
+            super.showShareDetailView(fetched)
+        }
+    }
+
+    override fun showActivityDetailView(file: OCFile) {
+        fetchFileAndRun(file) { fetched ->
+            super.showActivityDetailView(fetched)
+        }
+    }
+
+    override fun onOverflowIconClicked(file: OCFile, view: View?) {
+        fetchFileAndRun(file) { fetched ->
+            super.onOverflowIconClicked(fetched, view)
+        }
+    }
+
+    override fun onItemClicked(file: OCFile) {
+        fetchFileAndRun(file) { fetched ->
+            super.onItemClicked(fetched)
+        }
+    }
+
+    override fun onLongItemClicked(file: OCFile): Boolean {
+        fetchFileAndRun(file) { fetched ->
+            super.onLongItemClicked(fetched)
+        }
+        return true
+    }
+
+    companion object {
+        private val SHARED_TAG = SharedListFragment::class.java.simpleName
+    }
+}

+ 129 - 0
app/src/test/java/com/owncloud/android/ui/adapter/OCShareToOCFileConverterTest.kt

@@ -0,0 +1,129 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Álvaro Brey Vilas
+ * Copyright (C) 2022 Álvaro Brey Vilas
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.ui.adapter
+
+import com.owncloud.android.lib.resources.shares.OCShare
+import com.owncloud.android.lib.resources.shares.ShareType
+import org.junit.Assert
+import org.junit.Test
+
+class OCShareToOCFileConverterTest {
+
+    @Test
+    fun testSingleOCShare() {
+        val shares = listOf(
+            OCShare("/foo")
+                .apply {
+                    shareType = ShareType.PUBLIC_LINK
+                }
+        )
+
+        val result = OCShareToOCFileConverter.buildOCFilesFromShares(shares)
+
+        Assert.assertEquals("Wrong file list size", 1, result.size)
+        val ocFile = result[0]
+        Assert.assertEquals("Wrong file path", "/foo", ocFile.remotePath)
+        Assert.assertEquals("File should have link attribute", true, ocFile.isSharedViaLink)
+        Assert.assertEquals("File should not have sharee attribute", false, ocFile.isSharedWithSharee)
+    }
+
+    @Test
+    fun testMultipleSharesSamePath() {
+        val shares = listOf(
+            OCShare("/foo")
+                .apply {
+                    shareType = ShareType.PUBLIC_LINK
+                    sharedDate = 10
+                },
+            OCShare("/foo")
+                .apply {
+                    shareType = ShareType.EMAIL
+                    sharedDate = 22
+                },
+            OCShare("/foo")
+                .apply {
+                    shareType = ShareType.INTERNAL
+                    sharedDate = 11
+                    shareWith = "abcd"
+                    sharedWithDisplayName = "Ab Cd"
+                }
+        )
+
+        val result = OCShareToOCFileConverter.buildOCFilesFromShares(shares)
+
+        Assert.assertEquals("Wrong file list size", 1, result.size)
+        val ocFile = result[0]
+        Assert.assertEquals("Wrong file path", "/foo", ocFile.remotePath)
+        Assert.assertEquals("File should have link attribute", true, ocFile.isSharedViaLink)
+        Assert.assertEquals("File should have sharee attribute", true, ocFile.isSharedWithSharee)
+        Assert.assertEquals("Wrong name of sharees", 1, ocFile.sharees.size)
+        Assert.assertEquals("Wrong shared timestamp", 10000, ocFile.firstShareTimestamp)
+    }
+
+    @Test
+    fun testMultipleSharesMultiplePaths() {
+        val shares = listOf(
+            OCShare("/foo")
+                .apply {
+                    shareType = ShareType.INTERNAL
+                    sharedDate = 10
+                    shareWith = "aabc"
+                    sharedWithDisplayName = "Aa Bc"
+                },
+            OCShare("/foo")
+                .apply {
+                    shareType = ShareType.INTERNAL
+                    sharedDate = 22
+                    shareWith = "cccc"
+                    sharedWithDisplayName = "Cc Cc"
+                },
+            OCShare("/foo")
+                .apply {
+                    shareType = ShareType.INTERNAL
+                    sharedDate = 11
+                    shareWith = "abcd"
+                    sharedWithDisplayName = "Ab Cd"
+                },
+            OCShare("/bar")
+                .apply {
+                    shareType = ShareType.EMAIL
+                    sharedDate = 5
+                },
+        )
+
+        val result = OCShareToOCFileConverter.buildOCFilesFromShares(shares)
+
+        Assert.assertEquals("Wrong file list size", 2, result.size)
+
+        val ocFile = result[0]
+        Assert.assertEquals("Wrong file path", "/foo", ocFile.remotePath)
+        Assert.assertEquals("File should have no link attribute", false, ocFile.isSharedViaLink)
+        Assert.assertEquals("File should have sharee attribute", true, ocFile.isSharedWithSharee)
+        Assert.assertEquals("Wrong name of sharees", 3, ocFile.sharees.size)
+        Assert.assertEquals("Wrong shared timestamp", 10000, ocFile.firstShareTimestamp)
+
+        val ocFile2 = result[1]
+        Assert.assertEquals("Wrong file path", "/bar", ocFile2.remotePath)
+        Assert.assertEquals("File should have link attribute", true, ocFile2.isSharedViaLink)
+        Assert.assertEquals("File should have no sharee attribute", false, ocFile2.isSharedWithSharee)
+    }
+}