소스 검색

Merge pull request #11526 from nextcloud/groupfolders

Groupfolders
Andy Scherzinger 2 년 전
부모
커밋
1fc07883e2
22개의 변경된 파일540개의 추가작업 그리고 8개의 파일을 삭제
  1. 0 2
      .idea/codeStyles/Project.xml
  2. BIN
      app/screenshots/gplay/debug/com.nextcloud.client.ActivitiesActivityIT_openDrawer.png
  3. BIN
      app/screenshots/gplay/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_drawer.png
  4. BIN
      app/screenshots/gplay/debug/com.nextcloud.client.UploadListActivityActivityIT_openDrawer.png
  5. BIN
      app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testBottomSheet.png
  6. BIN
      app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.GroupfolderListFragmentIT_showGroupfolders.png
  7. 78 0
      app/src/androidTest/java/com/owncloud/android/ui/fragment/GroupfolderListFragmentIT.kt
  8. 4 0
      app/src/main/java/com/nextcloud/client/di/ComponentsModule.java
  9. 1 0
      app/src/main/java/com/nextcloud/client/etm/EtmActivity.kt
  10. 10 0
      app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java
  11. 69 2
      app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java
  12. 90 0
      app/src/main/java/com/owncloud/android/ui/adapter/GroupfolderListAdapter.kt
  13. 72 0
      app/src/main/java/com/owncloud/android/ui/asynctasks/GroupfoldersSearchTask.kt
  14. 1 2
      app/src/main/java/com/owncloud/android/ui/fragment/GalleryFragment.java
  15. 166 0
      app/src/main/java/com/owncloud/android/ui/fragment/GroupfolderListFragment.kt
  16. 7 1
      app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchAsyncTask.kt
  17. 2 1
      app/src/main/java/com/owncloud/android/ui/fragment/SearchType.kt
  18. 27 0
      app/src/main/java/com/owncloud/android/ui/interfaces/GroupfolderListInterface.kt
  19. 6 0
      app/src/main/java/com/owncloud/android/utils/DrawerMenuUtil.java
  20. 1 0
      app/src/main/res/layout/list_item.xml
  21. 5 0
      app/src/main/res/menu/partial_drawer_entries.xml
  22. 1 0
      app/src/main/res/values/strings.xml

+ 0 - 2
.idea/codeStyles/Project.xml

@@ -58,8 +58,6 @@
           <package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
         </value>
       </option>
-      <option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
-      <option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
       <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
     </JetCodeStyleSettings>
     <MarkdownNavigatorCodeStyleSettings>

BIN
app/screenshots/gplay/debug/com.nextcloud.client.ActivitiesActivityIT_openDrawer.png


BIN
app/screenshots/gplay/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_drawer.png


BIN
app/screenshots/gplay/debug/com.nextcloud.client.UploadListActivityActivityIT_openDrawer.png


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


BIN
app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.GroupfolderListFragmentIT_showGroupfolders.png


+ 78 - 0
app/src/androidTest/java/com/owncloud/android/ui/fragment/GroupfolderListFragmentIT.kt

@@ -0,0 +1,78 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2023 Tobias Kaminsky
+ * Copyright (C) 2023 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 com.nextcloud.android.lib.resources.groupfolders.Groupfolder
+import com.nextcloud.test.TestActivity
+import com.owncloud.android.AbstractIT
+import com.owncloud.android.utils.ScreenshotTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+class GroupfolderListFragmentIT : AbstractIT() {
+    @get:Rule
+    val testActivityRule = IntentsTestRule(TestActivity::class.java, true, false)
+
+    lateinit var activity: TestActivity
+
+    @Before
+    fun before() {
+        activity = testActivityRule.launchActivity(null)
+    }
+
+    @ScreenshotTest
+    @Test
+    fun showEmpty() {
+        val sut = GroupfolderListFragment()
+        activity.addFragment(sut)
+
+        waitForIdleSync()
+
+        screenshot(activity)
+    }
+
+    @Test
+    @ScreenshotTest
+    fun showGroupfolders() {
+        val sut = GroupfolderListFragment()
+        activity.addFragment(sut)
+
+        waitForIdleSync()
+
+        activity.runOnUiThread {
+            sut.setAdapter(null)
+            sut.setData(
+                mapOf(
+                    Pair("1", Groupfolder(1, "/test/")),
+                    Pair("2", Groupfolder(2, "/subfolder/group"))
+                )
+            )
+        }
+
+        waitForIdleSync()
+        shortSleep()
+        screenshot(activity)
+    }
+}

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

@@ -109,6 +109,7 @@ import com.owncloud.android.ui.fragment.FileDetailSharingFragment;
 import com.owncloud.android.ui.fragment.FileDetailsSharingProcessFragment;
 import com.owncloud.android.ui.fragment.GalleryFragment;
 import com.owncloud.android.ui.fragment.GalleryFragmentBottomSheetDialog;
+import com.owncloud.android.ui.fragment.GroupfolderListFragment;
 import com.owncloud.android.ui.fragment.LocalFileListFragment;
 import com.owncloud.android.ui.fragment.OCFileListBottomSheetDialog;
 import com.owncloud.android.ui.fragment.OCFileListFragment;
@@ -462,4 +463,7 @@ abstract class ComponentsModule {
 
     @ContributesAndroidInjector
     abstract DocumentScanActivity documentScanActivity();
+
+    @ContributesAndroidInjector
+    abstract GroupfolderListFragment groupfolderListFragment();
 }

+ 1 - 0
app/src/main/java/com/nextcloud/client/etm/EtmActivity.kt

@@ -71,6 +71,7 @@ class EtmActivity : ToolbarActivity(), Injectable {
         }
     }
 
+    @Deprecated("Deprecated in Java")
     override fun onBackPressed() {
         if (!vm.onBackPressed()) {
             super.onBackPressed()

+ 10 - 0
app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java

@@ -100,6 +100,7 @@ import com.owncloud.android.ui.events.DummyDrawerEvent;
 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.GroupfolderListFragment;
 import com.owncloud.android.ui.fragment.OCFileListFragment;
 import com.owncloud.android.ui.fragment.SharedListFragment;
 import com.owncloud.android.ui.preview.PreviewTextStringFragment;
@@ -398,6 +399,7 @@ public abstract class DrawerActivity extends ToolbarActivity
         DrawerMenuUtil.filterSearchMenuItems(menu, user, getResources());
         DrawerMenuUtil.filterTrashbinMenuItem(menu, capability);
         DrawerMenuUtil.filterActivityMenuItem(menu, capability);
+        DrawerMenuUtil.filterGroupfoldersMenuItem(menu, capability);
 
         DrawerMenuUtil.setupHomeMenuItem(menu, getResources());
 
@@ -422,6 +424,7 @@ public abstract class DrawerActivity extends ToolbarActivity
             if (this instanceof FileDisplayActivity &&
                 !(((FileDisplayActivity) this).getLeftFragment() instanceof GalleryFragment) &&
                 !(((FileDisplayActivity) this).getLeftFragment() instanceof SharedListFragment) &&
+                !(((FileDisplayActivity) this).getLeftFragment() instanceof GroupfolderListFragment) &&
                 !(((FileDisplayActivity) this).getLeftFragment() instanceof PreviewTextStringFragment)) {
                 showFiles(false);
                 ((FileDisplayActivity) this).browseToRoot();
@@ -465,6 +468,13 @@ public abstract class DrawerActivity extends ToolbarActivity
             startSharedSearch(menuItem);
         } else if (itemId == R.id.nav_recently_modified) {
             startRecentlyModifiedSearch(menuItem);
+        } else if (itemId == R.id.nav_groupfolders) {
+            MainApp.showOnlyFilesOnDevice(false);
+            Intent intent = new Intent(getApplicationContext(), FileDisplayActivity.class);
+            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+            intent.setAction(FileDisplayActivity.LIST_GROUPFOLDERS);
+            intent.putExtra(FileDisplayActivity.DRAWER_MENU_ID, menuItem.getItemId());
+            startActivity(intent);
         } else {
             if (menuItem.getItemId() >= MENU_ITEM_EXTERNAL_LINK &&
                 menuItem.getItemId() <= MENU_ITEM_EXTERNAL_LINK + 100) {

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

@@ -59,9 +59,11 @@ import com.google.android.material.appbar.AppBarLayout;
 import com.google.android.material.snackbar.Snackbar;
 import com.nextcloud.client.account.User;
 import com.nextcloud.client.appinfo.AppInfo;
+import com.nextcloud.client.core.AsyncRunner;
 import com.nextcloud.client.di.Injectable;
 import com.nextcloud.client.files.DeepLinkHandler;
 import com.nextcloud.client.media.PlayerServiceConnection;
+import com.nextcloud.client.network.ClientFactory;
 import com.nextcloud.client.network.ConnectivityService;
 import com.nextcloud.client.preferences.AppPreferences;
 import com.nextcloud.client.utils.IntentUtil;
@@ -78,6 +80,7 @@ import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder;
 import com.owncloud.android.files.services.FileUploader;
 import com.owncloud.android.files.services.FileUploader.FileUploaderBinder;
 import com.owncloud.android.files.services.NameCollisionPolicy;
+import com.owncloud.android.lib.common.OwnCloudClient;
 import com.owncloud.android.lib.common.operations.RemoteOperation;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
@@ -96,16 +99,17 @@ import com.owncloud.android.operations.UploadFileOperation;
 import com.owncloud.android.syncadapter.FileSyncAdapter;
 import com.owncloud.android.ui.asynctasks.CheckAvailableSpaceTask;
 import com.owncloud.android.ui.asynctasks.FetchRemoteFileTask;
+import com.owncloud.android.ui.asynctasks.GetRemoteFileTask;
 import com.owncloud.android.ui.dialog.SendShareDialog;
 import com.owncloud.android.ui.dialog.SortingOrderDialogFragment;
 import com.owncloud.android.ui.dialog.StoragePermissionDialogFragment;
-import com.owncloud.android.ui.events.ChangeMenuEvent;
 import com.owncloud.android.ui.events.SearchEvent;
 import com.owncloud.android.ui.events.SyncEventFinished;
 import com.owncloud.android.ui.events.TokenPushEvent;
 import com.owncloud.android.ui.fragment.FileDetailFragment;
 import com.owncloud.android.ui.fragment.FileFragment;
 import com.owncloud.android.ui.fragment.GalleryFragment;
+import com.owncloud.android.ui.fragment.GroupfolderListFragment;
 import com.owncloud.android.ui.fragment.OCFileListFragment;
 import com.owncloud.android.ui.fragment.SearchType;
 import com.owncloud.android.ui.fragment.SharedListFragment;
@@ -150,6 +154,7 @@ import androidx.fragment.app.FragmentManager;
 import androidx.fragment.app.FragmentTransaction;
 import androidx.localbroadcastmanager.content.LocalBroadcastManager;
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import kotlin.Unit;
 
 import static com.owncloud.android.datamodel.OCFile.PATH_SEPARATOR;
 import static com.owncloud.android.utils.PermissionUtil.PERMISSION_CHOICE_DIALOG_TAG;
@@ -164,6 +169,7 @@ public class FileDisplayActivity extends FileActivity
 
     public static final String RESTART = "RESTART";
     public static final String ALL_FILES = "ALL_FILES";
+    public static final String LIST_GROUPFOLDERS = "LIST_GROUPFOLDERS";
     public static final String PHOTO_SEARCH = "PHOTO_SEARCH";
     public static final int SINGLE_USER_SIZE = 1;
     public static final String OPEN_FILE = "NC_OPEN_FILE";
@@ -180,6 +186,7 @@ public class FileDisplayActivity extends FileActivity
     public static final String TAG_PUBLIC_LINK = "PUBLIC_LINK";
     public static final String FTAG_CHOOSER_DIALOG = "CHOOSER_DIALOG";
     public static final String KEY_FILE_ID = "KEY_FILE_ID";
+    public static final String KEY_FILE_PATH = "KEY_FILE_PATH";
     public static final String KEY_ACCOUNT = "KEY_ACCOUNT";
 
 
@@ -236,6 +243,7 @@ public class FileDisplayActivity extends FileActivity
 
     @Inject
     FastScrollUtils fastScrollUtils;
+    @Inject AsyncRunner asyncRunner;
 
     public static Intent openFileIntent(Context context, User user, OCFile file) {
         final Intent intent = new Intent(context, PreviewImageActivity.class);
@@ -551,6 +559,11 @@ public class FileDisplayActivity extends FileActivity
                 setLeftFragment(new OCFileListFragment());
                 getSupportFragmentManager().executePendingTransactions();
                 browseToRoot();
+            } else if (LIST_GROUPFOLDERS.equals(intent.getAction())) {
+                Log_OC.d(this, "Switch to list groupfolders fragment");
+
+                setLeftFragment(new GroupfolderListFragment());
+                getSupportFragmentManager().executePendingTransactions();
             }
     }
 
@@ -2433,13 +2446,21 @@ public class FileDisplayActivity extends FileActivity
 
         String userName = intent.getStringExtra(KEY_ACCOUNT);
         String fileId = intent.getStringExtra(KEY_FILE_ID);
+        String filePath = intent.getStringExtra(KEY_FILE_PATH);
 
         if (userName == null && fileId == null && intent.getData() != null) {
             openDeepLink(intent.getData());
         } else {
             Optional<User> optionalUser = userName == null ? getUser() : getUserAccountManager().getUser(userName);
             if (optionalUser.isPresent()) {
-                openFile(optionalUser.get(), fileId);
+                if (!TextUtils.isEmpty(fileId)) {
+                    openFile(optionalUser.get(), fileId);
+                } else if (!TextUtils.isEmpty(filePath)) {
+                    openFileByPath(optionalUser.get(), filePath);
+                } else {
+                    dismissLoadingDialog();
+                    DisplayUtils.showSnackMessage(this, getString(R.string.file_not_found));
+                }
             } else {
                 dismissLoadingDialog();
                 DisplayUtils.showSnackMessage(this, getString(R.string.associated_account_not_found));
@@ -2501,7 +2522,53 @@ public class FileDisplayActivity extends FileActivity
                                                                           storageManager,
                                                                           this);
         fetchRemoteFileTask.execute();
+    }
+
+    private void openFileByPath(User user, String filepath) {
+        setUser(user);
+
+        if (filepath == null) {
+            dismissLoadingDialog();
+            DisplayUtils.showSnackMessage(this, getString(R.string.error_retrieving_file));
+            return;
+        }
+
+        FileDataStorageManager storageManager = getStorageManager();
+
+        if (storageManager == null) {
+            storageManager = new FileDataStorageManager(user, getContentResolver());
+        }
+
+        OwnCloudClient client;
+        try {
+            client = clientFactory.create(user);
+        } catch (ClientFactory.CreationException e) {
+            dismissLoadingDialog();
+            DisplayUtils.showSnackMessage(this, getString(R.string.error_retrieving_file));
+            return;
+        }
+
+        GetRemoteFileTask getRemoteFileTask = new GetRemoteFileTask(this,
+                                                                    filepath,
+                                                                    client,
+                                                                    storageManager,
+                                                                    user);
+        asyncRunner.postQuickTask(getRemoteFileTask, this::onFileRequestResult, null);
+    }
+
+    private Unit onFileRequestResult(GetRemoteFileTask.Result result) {
+        dismissLoadingDialog();
+
+        setFile(result.getFile());
 
+        OCFileListFragment fileFragment = new OCFileListFragment();
+        setLeftFragment(fileFragment);
+
+        getSupportFragmentManager().executePendingTransactions();
+
+        fileFragment.onItemClicked(result.getFile());
+
+        return null;
     }
 
     public void performUnifiedSearch(String query) {

+ 90 - 0
app/src/main/java/com/owncloud/android/ui/adapter/GroupfolderListAdapter.kt

@@ -0,0 +1,90 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2023 Tobias Kaminsky
+ * Copyright (C) 2023 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 android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.appcompat.content.res.AppCompatResources
+import androidx.recyclerview.widget.RecyclerView
+import com.nextcloud.android.lib.resources.groupfolders.Groupfolder
+import com.owncloud.android.R
+import com.owncloud.android.databinding.ListItemBinding
+import com.owncloud.android.ui.interfaces.GroupfolderListInterface
+import com.owncloud.android.utils.theme.ViewThemeUtils
+import java.io.File
+
+class GroupfolderListAdapter(
+    val context: Context,
+    val viewThemeUtils: ViewThemeUtils,
+    private val groupfolderListInterface: GroupfolderListInterface
+) :
+    RecyclerView.Adapter<RecyclerView.ViewHolder>() {
+    lateinit var list: List<Groupfolder>
+
+    private val folderIcon = viewThemeUtils.platform.tintPrimaryDrawable(
+        context,
+        AppCompatResources.getDrawable(
+            context,
+            R.drawable.folder_group
+        )
+    )
+
+    fun setData(result: Map<String, Groupfolder>) {
+        list = result.values.sortedBy { it.mountPoint }
+    }
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
+        return OCFileListItemViewHolder(
+            ListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+        )
+    }
+
+    override fun getItemCount(): Int {
+        return list.size
+    }
+
+    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+        val groupfolder = list[position]
+        val listHolder = holder as OCFileListItemViewHolder
+
+        val file = File("/" + groupfolder.mountPoint)
+
+        listHolder.apply {
+            fileName.text = file.name
+            fileSize.text = file.parentFile?.path ?: "/"
+            fileSizeSeparator.visibility = View.GONE
+            lastModification.visibility = View.GONE
+            checkbox.visibility = View.GONE
+            overflowMenu.visibility = View.GONE
+            shared.visibility = View.GONE
+            localFileIndicator.visibility = View.GONE
+            favorite.visibility = View.GONE
+
+            thumbnail.setImageDrawable(folderIcon)
+
+            itemLayout.setOnClickListener { groupfolderListInterface.onFolderClick(groupfolder.mountPoint) }
+        }
+    }
+}

+ 72 - 0
app/src/main/java/com/owncloud/android/ui/asynctasks/GroupfoldersSearchTask.kt

@@ -0,0 +1,72 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2023 Tobias Kaminsky
+ * Copyright (C) 2023 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.asynctasks
+
+import android.os.AsyncTask
+import com.nextcloud.android.lib.resources.groupfolders.GetGroupfoldersRemoteOperation
+import com.nextcloud.android.lib.resources.groupfolders.Groupfolder
+import com.nextcloud.client.account.User
+import com.owncloud.android.datamodel.FileDataStorageManager
+import com.owncloud.android.ui.fragment.GroupfolderListFragment
+import java.lang.ref.WeakReference
+
+class GroupfoldersSearchTask(
+    fragment: GroupfolderListFragment,
+    private val user: User,
+    storageManager: FileDataStorageManager
+) : AsyncTask<Void, Void, Map<String, Groupfolder>>() {
+    private val fragmentWeakReference: WeakReference<GroupfolderListFragment?>
+    private val storageManager: FileDataStorageManager
+
+    init {
+        fragmentWeakReference = WeakReference(fragment)
+        this.storageManager = storageManager
+    }
+
+    override fun doInBackground(vararg voids: Void): Map<String, Groupfolder> {
+        if (fragmentWeakReference.get() == null) {
+            return HashMap()
+        }
+        val fragment = fragmentWeakReference.get()
+        return if (isCancelled) {
+            HashMap()
+        } else {
+            val searchRemoteOperation = GetGroupfoldersRemoteOperation()
+            if (fragment?.context != null) {
+                val result = searchRemoteOperation.executeNextcloudClient(
+                    user,
+                    fragment.requireContext()
+                )
+                if (result.isSuccess) {
+                    result.resultData
+                } else {
+                    HashMap()
+                }
+            } else {
+                HashMap()
+            }
+        }
+    }
+
+    override fun onPostExecute(result: Map<String, Groupfolder>) {
+        fragmentWeakReference.get()?.setData(result)
+    }
+}

+ 1 - 2
app/src/main/java/com/owncloud/android/ui/fragment/GalleryFragment.java

@@ -23,7 +23,6 @@
 
 package com.owncloud.android.ui.fragment;
 
-import android.annotation.SuppressLint;
 import android.content.Intent;
 import android.content.res.Configuration;
 import android.os.AsyncTask;
@@ -398,4 +397,4 @@ public class GalleryFragment extends OCFileListFragment implements GalleryFragme
     protected void setGridViewColumns(float scaleFactor) {
         // do nothing
     }
-}
+}

+ 166 - 0
app/src/main/java/com/owncloud/android/ui/fragment/GroupfolderListFragment.kt

@@ -0,0 +1,166 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2023 Tobias Kaminsky
+ * Copyright (C) 2023 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.annotation.SuppressLint
+import android.content.Intent
+import android.content.Intent.ACTION_VIEW
+import android.os.Bundle
+import android.os.Handler
+import androidx.lifecycle.lifecycleScope
+import androidx.recyclerview.widget.GridLayoutManager
+import com.nextcloud.android.lib.resources.groupfolders.Groupfolder
+import com.nextcloud.client.di.Injectable
+import com.nextcloud.client.logger.Logger
+import com.owncloud.android.MainApp
+import com.owncloud.android.R
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation
+import com.owncloud.android.lib.resources.files.model.RemoteFile
+import com.owncloud.android.ui.activity.FileDisplayActivity
+import com.owncloud.android.ui.adapter.GroupfolderListAdapter
+import com.owncloud.android.ui.asynctasks.GroupfoldersSearchTask
+import com.owncloud.android.ui.interfaces.GroupfolderListInterface
+import com.owncloud.android.utils.DisplayUtils
+import com.owncloud.android.utils.FileStorageUtils
+import com.owncloud.android.utils.theme.ViewThemeUtils
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import javax.inject.Inject
+
+/**
+ * A Fragment that lists groupfolders
+ */
+class GroupfolderListFragment : OCFileListFragment(), Injectable, GroupfolderListInterface {
+
+    lateinit var adapter: GroupfolderListAdapter
+
+    @Inject
+    lateinit var logger: Logger
+
+    @Inject
+    lateinit var viewThemeUtils: ViewThemeUtils
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        searchFragment = true
+    }
+
+    override fun onActivityCreated(savedInstanceState: Bundle?) {
+        super.onActivityCreated(savedInstanceState)
+
+        currentSearchType = SearchType.GROUPFOLDER
+        menuItemAddRemoveValue = MenuItemAddRemove.REMOVE_GRID_AND_SORT
+        requireActivity().invalidateOptionsMenu()
+
+        search()
+    }
+
+    public override fun setAdapter(args: Bundle?) {
+        adapter = GroupfolderListAdapter(requireContext(), viewThemeUtils, this)
+        setRecyclerViewAdapter(adapter)
+
+        val layoutManager = GridLayoutManager(context, 1)
+        recyclerView.layoutManager = layoutManager
+    }
+
+    private fun search() {
+        GroupfoldersSearchTask(
+            this,
+            accountManager.user,
+            mContainerActivity.storageManager
+        ).execute()
+    }
+
+    override fun onResume() {
+        super.onResume()
+        Handler().post {
+            if (activity is FileDisplayActivity) {
+                val fileDisplayActivity = activity as FileDisplayActivity
+                fileDisplayActivity.updateActionBarTitleAndHomeButtonByString(
+                    getString(R.string.drawer_item_groupfolders)
+                )
+                fileDisplayActivity.setMainFabVisible(false)
+            }
+        }
+    }
+
+    @SuppressLint("NotifyDataSetChanged")
+    fun setData(result: Map<String, Groupfolder>) {
+        adapter.setData(result)
+        adapter.notifyDataSetChanged()
+    }
+
+    private suspend fun fetchFileData(partialFile: OCFile): OCFile? {
+        return withContext(Dispatchers.IO) {
+            val user = accountManager.user
+            val fetchResult = ReadFileRemoteOperation(partialFile.remotePath).execute(user, 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)
+            }
+        }
+    }
+
+    companion object {
+        private val SHARED_TAG = GroupfolderListFragment::class.java.simpleName
+    }
+
+    override fun onFolderClick(path: String) {
+        MainApp.showOnlyFilesOnDevice(false)
+        Intent(
+            context,
+            FileDisplayActivity::class.java
+        ).apply {
+            addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+            action = ACTION_VIEW
+            putExtra(FileDisplayActivity.KEY_FILE_PATH, path)
+            startActivity(this)
+        }
+    }
+}

+ 7 - 1
app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchAsyncTask.kt

@@ -61,7 +61,13 @@ class OCFileListSearchAsyncTask(
         }
 
         fragment.setTitle()
-        val remoteOperationResult = remoteOperation.execute(currentUser, fragment.context)
+        lateinit var remoteOperationResult: RemoteOperationResult<List<Any>>
+        try {
+            remoteOperationResult = remoteOperation.execute(currentUser, fragment.context)
+        } catch (e: UnsupportedOperationException) {
+            remoteOperationResult = remoteOperation.executeNextcloudClient(currentUser, fragment.requireContext())
+        }
+
         if (remoteOperationResult.hasSuccessfulResult() && !isCancelled && fragment.searchFragment) {
             fragment.searchEvent = event
             if (remoteOperationResult.resultData.isNullOrEmpty()) {

+ 2 - 1
app/src/main/java/com/owncloud/android/ui/fragment/SearchType.kt

@@ -13,5 +13,6 @@ enum class SearchType : Parcelable {
     RECENTLY_MODIFIED_SEARCH,
 
     // not a real filter, but nevertheless
-    SHARED_FILTER
+    SHARED_FILTER,
+    GROUPFOLDER
 }

+ 27 - 0
app/src/main/java/com/owncloud/android/ui/interfaces/GroupfolderListInterface.kt

@@ -0,0 +1,27 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2023 Tobias Kaminsky
+ * Copyright (C) 2023 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.interfaces
+
+interface GroupfolderListInterface {
+    fun onFolderClick(path: String)
+}

+ 6 - 0
app/src/main/java/com/owncloud/android/utils/DrawerMenuUtil.java

@@ -64,6 +64,12 @@ public final class DrawerMenuUtil {
         }
     }
 
+    public static void filterGroupfoldersMenuItem(Menu menu, @Nullable OCCapability capability) {
+        if (capability != null && capability.getGroupfolders().isFalse()) {
+            filterMenuItems(menu, R.id.nav_groupfolders);
+        }
+    }
+
     public static void removeMenuItem(Menu menu, int id, boolean remove) {
         if (remove) {
             menu.removeItem(id);

+ 1 - 0
app/src/main/res/layout/list_item.xml

@@ -107,6 +107,7 @@
                 android:layout_height="wrap_content"
                 android:clickable="false"
                 android:paddingTop="@dimen/standard_quarter_padding"
+                android:visibility="gone"
                 app:chipSpacingVertical="@dimen/standard_quarter_padding">
 
                 <com.google.android.material.chip.Chip

+ 5 - 0
app/src/main/res/menu/partial_drawer_entries.xml

@@ -48,6 +48,11 @@
             android:orderInCategory="0"
             android:icon="@drawable/nav_shared"
             android:title="@string/drawer_item_shared" />
+        <item
+            android:id="@+id/nav_groupfolders"
+            android:orderInCategory="0"
+            android:icon="@drawable/ic_group"
+            android:title="@string/drawer_item_groupfolders" />
         <item
             android:id="@+id/nav_on_device"
             android:icon="@drawable/nav_on_device"

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

@@ -1078,5 +1078,6 @@
     <string name="document_scan_export_dialog_images">Multiple images</string>
     <string name="download_cannot_create_file">Cannot create local file</string>
     <string name="download_download_invalid_local_file_name">Invalid filename for local file</string>
+    <string name="drawer_item_groupfolders">Groupfolders</string>
     <string name="tags_more">+%1$d</string>
 </resources>