Browse Source

Merge pull request #13167 from nextcloud/crash-fix-synced-folder-adapter

Crash Fix & Convert to Kotlin Synced Folder Adapter
Alper Öztürk 9 months ago
parent
commit
38addfcdcc

+ 15 - 7
app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt

@@ -222,11 +222,12 @@ class SyncedFoldersActivity :
 
 
     private fun showPowerCheckDialog() {
     private fun showPowerCheckDialog() {
         val alertDialog = AlertDialog.Builder(this)
         val alertDialog = AlertDialog.Builder(this)
-            .setView(findViewById(R.id.root_layout))
+            .setView(R.id.root_layout)
             .setPositiveButton(R.string.common_ok) { dialog, _ -> dialog.dismiss() }
             .setPositiveButton(R.string.common_ok) { dialog, _ -> dialog.dismiss() }
             .setTitle(R.string.autoupload_disable_power_save_check)
             .setTitle(R.string.autoupload_disable_power_save_check)
             .setMessage(getString(R.string.power_save_check_dialog_message))
             .setMessage(getString(R.string.power_save_check_dialog_message))
             .show()
             .show()
+
         viewThemeUtils.platform.colorTextButtons(alertDialog.getButton(AlertDialog.BUTTON_POSITIVE))
         viewThemeUtils.platform.colorTextButtons(alertDialog.getButton(AlertDialog.BUTTON_POSITIVE))
     }
     }
 
 
@@ -307,7 +308,8 @@ class SyncedFoldersActivity :
             }
             }
             val syncFolderItems = sortSyncedFolderItems(
             val syncFolderItems = sortSyncedFolderItems(
                 mergeFolderData(currentAccountSyncedFoldersList, mediaFolders)
                 mergeFolderData(currentAccountSyncedFoldersList, mediaFolders)
-            )
+            ).filterNotNull()
+
             CoroutineScope(Dispatchers.Main).launch {
             CoroutineScope(Dispatchers.Main).launch {
                 adapter.setSyncFolderItems(syncFolderItems)
                 adapter.setSyncFolderItems(syncFolderItems)
                 adapter.notifyDataSetChanged()
                 adapter.notifyDataSetChanged()
@@ -315,7 +317,9 @@ class SyncedFoldersActivity :
                 if (!TextUtils.isEmpty(path)) {
                 if (!TextUtils.isEmpty(path)) {
                     val section = adapter.getSectionByLocalPathAndType(path, type)
                     val section = adapter.getSectionByLocalPathAndType(path, type)
                     if (section >= 0) {
                     if (section >= 0) {
-                        onSyncFolderSettingsClick(section, adapter[section])
+                        adapter.get(section)?.let {
+                            onSyncFolderSettingsClick(section, it)
+                        }
                     }
                     }
                 }
                 }
                 loadJob = null
                 loadJob = null
@@ -559,7 +563,9 @@ class SyncedFoldersActivity :
         return result
         return result
     }
     }
 
 
-    override fun onSyncStatusToggleClick(section: Int, syncedFolderDisplayItem: SyncedFolderDisplayItem) {
+    override fun onSyncStatusToggleClick(section: Int, syncedFolderDisplayItem: SyncedFolderDisplayItem?) {
+        if (syncedFolderDisplayItem == null) return
+
         if (syncedFolderDisplayItem.id > SyncedFolder.UNPERSISTED_ID) {
         if (syncedFolderDisplayItem.id > SyncedFolder.UNPERSISTED_ID) {
             syncedFolderProvider.updateSyncedFolderEnabled(
             syncedFolderProvider.updateSyncedFolderEnabled(
                 syncedFolderDisplayItem.id,
                 syncedFolderDisplayItem.id,
@@ -577,7 +583,7 @@ class SyncedFoldersActivity :
         }
         }
     }
     }
 
 
-    override fun onSyncFolderSettingsClick(section: Int, syncedFolderDisplayItem: SyncedFolderDisplayItem) {
+    override fun onSyncFolderSettingsClick(section: Int, syncedFolderDisplayItem: SyncedFolderDisplayItem?) {
         val fragmentTransaction = supportFragmentManager.beginTransaction().apply {
         val fragmentTransaction = supportFragmentManager.beginTransaction().apply {
             addToBackStack(null)
             addToBackStack(null)
         }
         }
@@ -596,7 +602,9 @@ class SyncedFoldersActivity :
         }
         }
     }
     }
 
 
-    override fun onVisibilityToggleClick(section: Int, syncedFolder: SyncedFolderDisplayItem) {
+    override fun onVisibilityToggleClick(section: Int, syncedFolder: SyncedFolderDisplayItem?) {
+        if (syncedFolder == null) return
+
         syncedFolder.isHidden = !syncedFolder.isHidden
         syncedFolder.isHidden = !syncedFolder.isHidden
         saveOrUpdateSyncedFolder(syncedFolder)
         saveOrUpdateSyncedFolder(syncedFolder)
         adapter.setSyncFolderItem(section, syncedFolder)
         adapter.setSyncFolderItem(section, syncedFolder)
@@ -676,7 +684,7 @@ class SyncedFoldersActivity :
             saveOrUpdateSyncedFolder(newCustomFolder)
             saveOrUpdateSyncedFolder(newCustomFolder)
             adapter.addSyncFolderItem(newCustomFolder)
             adapter.addSyncFolderItem(newCustomFolder)
         } else {
         } else {
-            val item = adapter[syncedFolder.section]
+            val item = adapter.get(syncedFolder.section) ?: return
             updateSyncedFolderItem(
             updateSyncedFolderItem(
                 item,
                 item,
                 syncedFolder.id,
                 syncedFolder.id,

+ 0 - 454
app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.java

@@ -1,454 +0,0 @@
-/*
- * Nextcloud - Android Client
- *
- * SPDX-FileCopyrightText: 2016 Andy Scherzinger
- * SPDX-FileCopyrightText: 2016 Nextcloud
- * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
- */
-package com.owncloud.android.ui.adapter;
-
-import android.content.Context;
-import android.view.LayoutInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageButton;
-import android.widget.PopupMenu;
-
-import com.afollestad.sectionedrecyclerview.SectionedRecyclerViewAdapter;
-import com.afollestad.sectionedrecyclerview.SectionedViewHolder;
-import com.nextcloud.client.core.Clock;
-import com.owncloud.android.R;
-import com.owncloud.android.databinding.GridSyncItemBinding;
-import com.owncloud.android.databinding.SyncedFoldersEmptyBinding;
-import com.owncloud.android.databinding.SyncedFoldersFooterBinding;
-import com.owncloud.android.databinding.SyncedFoldersItemHeaderBinding;
-import com.owncloud.android.datamodel.MediaFolderType;
-import com.owncloud.android.datamodel.SyncedFolderDisplayItem;
-import com.owncloud.android.datamodel.ThumbnailsCacheManager;
-import com.owncloud.android.utils.theme.ViewThemeUtils;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Locale;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
-
-/**
- * Adapter to display all auto-synced folders and/or instant upload media folders.
- */
-public class SyncedFolderAdapter extends SectionedRecyclerViewAdapter<SectionedViewHolder> {
-
-    private final Context context;
-    private final Clock clock;
-    private final int gridWidth;
-    private final int gridTotal;
-    private final ClickListener clickListener;
-    private final List<SyncedFolderDisplayItem> syncFolderItems;
-    private final List<SyncedFolderDisplayItem> filteredSyncFolderItems;
-    private final boolean light;
-    private static final int VIEW_TYPE_EMPTY = Integer.MAX_VALUE;
-    private static final int VIEW_TYPE_ITEM = 1;
-    private static final int VIEW_TYPE_HEADER = 2;
-    private static final int VIEW_TYPE_FOOTER = 3;
-    private boolean hideItems;
-    private final ViewThemeUtils viewThemeUtils;
-    private final Executor thumbnailThreadPool;
-
-    public SyncedFolderAdapter(Context context,
-                               Clock clock,
-                               int gridWidth,
-                               ClickListener listener,
-                               boolean light,
-                               ViewThemeUtils viewThemeUtils) {
-        this.context = context;
-        this.clock = clock;
-        this.gridWidth = gridWidth;
-        gridTotal = gridWidth * 2;
-        clickListener = listener;
-        syncFolderItems = new ArrayList<>();
-        filteredSyncFolderItems = new ArrayList<>();
-        this.light = light;
-        this.hideItems = true;
-        this.viewThemeUtils = viewThemeUtils;
-        this.thumbnailThreadPool = Executors.newCachedThreadPool();
-
-        shouldShowHeadersForEmptySections(true);
-        shouldShowFooters(true);
-    }
-
-    public void toggleHiddenItemsVisibility() {
-        hideItems = !hideItems;
-        filteredSyncFolderItems.clear();
-        filteredSyncFolderItems.addAll(filterHiddenItems(syncFolderItems, hideItems));
-        notifyDataSetChanged();
-    }
-
-    public void setSyncFolderItems(List<SyncedFolderDisplayItem> syncFolderItems) {
-        this.syncFolderItems.clear();
-        this.syncFolderItems.addAll(syncFolderItems);
-
-        this.filteredSyncFolderItems.clear();
-        this.filteredSyncFolderItems.addAll(filterHiddenItems(this.syncFolderItems, hideItems));
-    }
-
-    public void setSyncFolderItem(int location, SyncedFolderDisplayItem syncFolderItem) {
-        if (hideItems && syncFolderItem.isHidden() && filteredSyncFolderItems.contains(syncFolderItem)) {
-            filteredSyncFolderItems.remove(location);
-        } else {
-            if (filteredSyncFolderItems.contains(syncFolderItem)) {
-                filteredSyncFolderItems.set(filteredSyncFolderItems.indexOf(syncFolderItem), syncFolderItem);
-            } else {
-                filteredSyncFolderItems.add(syncFolderItem);
-            }
-        }
-
-        if (syncFolderItems.contains(syncFolderItem)) {
-            syncFolderItems.set(syncFolderItems.indexOf(syncFolderItem), syncFolderItem);
-        } else {
-            syncFolderItems.add(syncFolderItem);
-        }
-
-        notifyDataSetChanged();
-    }
-
-    public void addSyncFolderItem(SyncedFolderDisplayItem syncFolderItem) {
-        syncFolderItems.add(syncFolderItem);
-
-        // add item for display when either all items should be shown (!hideItems)
-        // or if item should be shown (!.isHidden())
-        if (!hideItems || !syncFolderItem.isHidden()) {
-            filteredSyncFolderItems.add(syncFolderItem);
-            notifyDataSetChanged();
-        }
-    }
-
-    public void removeItem(int section) {
-        if (filteredSyncFolderItems.contains(syncFolderItems.get(section))) {
-            filteredSyncFolderItems.remove(syncFolderItems.get(section));
-            notifyDataSetChanged();
-        }
-        syncFolderItems.remove(section);
-    }
-
-    /**
-     * Filter for hidden items
-     *
-     * @param items Collection of items to filter
-     * @return Non-hidden items
-     */
-    private List<SyncedFolderDisplayItem> filterHiddenItems(List<SyncedFolderDisplayItem> items, boolean hide) {
-        if (!hide) {
-            return items;
-        } else {
-            List<SyncedFolderDisplayItem> result = new ArrayList<>();
-
-            for (SyncedFolderDisplayItem item : items) {
-                if (!item.isHidden() && !result.contains(item)) {
-                    result.add(item);
-                }
-            }
-
-            return result;
-        }
-    }
-
-    @Override
-    public int getSectionCount() {
-        if (filteredSyncFolderItems.size() > 0) {
-            return filteredSyncFolderItems.size() + 1;
-        } else {
-            return 0;
-        }
-    }
-
-    @VisibleForTesting
-    public void clear() {
-        filteredSyncFolderItems.clear();
-        syncFolderItems.clear();
-    }
-
-    public int getUnfilteredSectionCount() {
-        if (syncFolderItems.size() > 0) {
-            return syncFolderItems.size() + 1;
-        } else {
-            return 0;
-        }
-    }
-
-    @Override
-    public int getItemCount(int section) {
-        if (section < filteredSyncFolderItems.size()) {
-            List<String> filePaths = filteredSyncFolderItems.get(section).getFilePaths();
-
-            if (filePaths != null) {
-                return filteredSyncFolderItems.get(section).getFilePaths().size();
-            } else {
-                return 1;
-            }
-        } else {
-            return 1;
-        }
-    }
-
-    public SyncedFolderDisplayItem get(int section) {
-        return filteredSyncFolderItems.get(section);
-    }
-
-    @Override
-    public int getItemViewType(int section, int relativePosition, int absolutePosition) {
-        if (isLastSection(section)) {
-            return VIEW_TYPE_EMPTY;
-        } else {
-            return VIEW_TYPE_ITEM;
-        }
-    }
-
-    @Override
-    public int getHeaderViewType(int section) {
-        if (isLastSection(section)) {
-            return VIEW_TYPE_EMPTY;
-        } else {
-            return VIEW_TYPE_HEADER;
-        }
-    }
-
-    @Override
-    public int getFooterViewType(int section) {
-        if (isLastSection(section) && showFooter()) {
-            return VIEW_TYPE_FOOTER;
-        } else {
-            // only show footer after last item and only if folders have been hidden
-            return VIEW_TYPE_EMPTY;
-        }
-    }
-
-    private boolean showFooter() {
-        return syncFolderItems.size() > filteredSyncFolderItems.size();
-    }
-
-    /**
-     * returns the section of a synced folder for the given local path and type.
-     *
-     * @param localPath the local path of the synced folder
-     * @param type      the of the synced folder
-     * @return the section index of the looked up synced folder, <code>-1</code> if not present
-     */
-    public int getSectionByLocalPathAndType(String localPath, int type) {
-        for (int i = 0; i < filteredSyncFolderItems.size(); i++) {
-            if (filteredSyncFolderItems.get(i).getLocalPath().equalsIgnoreCase(localPath) &&
-                filteredSyncFolderItems.get(i).getType().id == type) {
-                return i;
-            }
-        }
-
-        return -1;
-    }
-
-    @Override
-    public void onBindHeaderViewHolder(SectionedViewHolder commonHolder, final int section, boolean expanded) {
-        if (section < filteredSyncFolderItems.size()) {
-            HeaderViewHolder holder = (HeaderViewHolder) commonHolder;
-            holder.binding.headerContainer.setVisibility(View.VISIBLE);
-
-            holder.binding.title.setText(filteredSyncFolderItems.get(section).getFolderName());
-
-            if (MediaFolderType.VIDEO == filteredSyncFolderItems.get(section).getType()) {
-                holder.binding.type.setImageResource(R.drawable.video_32dp);
-            } else if (MediaFolderType.IMAGE == filteredSyncFolderItems.get(section).getType()) {
-                holder.binding.type.setImageResource(R.drawable.image_32dp);
-            } else {
-                holder.binding.type.setImageResource(R.drawable.folder_star_32dp);
-            }
-
-            holder.binding.syncStatusButton.setVisibility(View.VISIBLE);
-            holder.binding.syncStatusButton.setTag(section);
-            holder.binding.syncStatusButton.setOnClickListener(v -> {
-                filteredSyncFolderItems.get(section).setEnabled(
-                    !filteredSyncFolderItems.get(section).isEnabled(),
-                    clock.getCurrentTime()
-                );
-                setSyncButtonActiveIcon(
-                    holder.binding.syncStatusButton,
-                    filteredSyncFolderItems.get(section).isEnabled());
-                clickListener.onSyncStatusToggleClick(section, filteredSyncFolderItems.get(section));
-            });
-            setSyncButtonActiveIcon(holder.binding.syncStatusButton, filteredSyncFolderItems.get(section).isEnabled());
-
-            if (light) {
-                holder.binding.settingsButton.setVisibility(View.GONE);
-            } else {
-                holder.binding.settingsButton.setVisibility(View.VISIBLE);
-                holder.binding.settingsButton.setTag(section);
-                holder.binding.settingsButton.setOnClickListener(
-                    v -> onOverflowIconClicked(section, filteredSyncFolderItems.get(section), v));
-            }
-        }
-    }
-
-    private void onOverflowIconClicked(int section, SyncedFolderDisplayItem item, View view) {
-        PopupMenu popup = new PopupMenu(context, view);
-        popup.inflate(R.menu.synced_folders_adapter);
-        popup.setOnMenuItemClickListener(i -> optionsItemSelected(i, section, item));
-        popup.getMenu()
-            .findItem(R.id.action_auto_upload_folder_toggle_visibility)
-            .setChecked(item.isHidden());
-
-        popup.show();
-    }
-
-    private boolean optionsItemSelected(MenuItem menuItem, int section, SyncedFolderDisplayItem item) {
-        if (menuItem.getItemId() == R.id.action_auto_upload_folder_toggle_visibility) {
-            clickListener.onVisibilityToggleClick(section, item);
-        } else {
-            // default: R.id.action_create_custom_folder
-            clickListener.onSyncFolderSettingsClick(section, item);
-        }
-        return true;
-    }
-
-    @Override
-    public void onBindFooterViewHolder(SectionedViewHolder holder, int section) {
-        if (isLastSection(section) && showFooter()) {
-            FooterViewHolder footerHolder = (FooterViewHolder) holder;
-            footerHolder.binding.footerText.setOnClickListener(v -> toggleHiddenItemsVisibility());
-            footerHolder.binding.footerText.setText(
-                context.getResources().getQuantityString(
-                    R.plurals.synced_folders_show_hidden_folders,
-                    getHiddenFolderCount(),
-                    getHiddenFolderCount()
-                )
-            );
-        }
-    }
-
-    @Override
-    public void onBindViewHolder(SectionedViewHolder commonHolder, int section, int relativePosition,
-                                 int absolutePosition) {
-        if (section < filteredSyncFolderItems.size() && filteredSyncFolderItems.get(section).getFilePaths() != null) {
-            MainViewHolder holder = (MainViewHolder) commonHolder;
-
-            File file = new File(filteredSyncFolderItems.get(section).getFilePaths().get(relativePosition));
-
-            ThumbnailsCacheManager.MediaThumbnailGenerationTask task =
-                new ThumbnailsCacheManager.MediaThumbnailGenerationTask(holder.binding.thumbnail,
-                                                                        context,
-                                                                        viewThemeUtils);
-
-            ThumbnailsCacheManager.AsyncMediaThumbnailDrawable asyncDrawable =
-                    new ThumbnailsCacheManager.AsyncMediaThumbnailDrawable(
-                        context.getResources(),
-                        ThumbnailsCacheManager.mDefaultImg
-                    );
-            holder.binding.thumbnail.setImageDrawable(asyncDrawable);
-
-            task.executeOnExecutor(thumbnailThreadPool, file);
-
-            // set proper tag
-            holder.binding.thumbnail.setTag(file.hashCode());
-
-            holder.itemView.setTag(relativePosition % gridWidth);
-
-            if (filteredSyncFolderItems.get(section).getNumberOfFiles() > gridTotal &&
-                relativePosition >= gridTotal - 1) {
-                holder.binding.counter.setText(
-                    String.format(
-                        Locale.US,
-                        "%d",
-                        filteredSyncFolderItems.get(section).getNumberOfFiles() - gridTotal));
-                holder.binding.counterLayout.setVisibility(View.VISIBLE);
-                holder.binding.thumbnailDarkener.setVisibility(View.VISIBLE);
-            } else {
-                holder.binding.counterLayout.setVisibility(View.GONE);
-                holder.binding.thumbnailDarkener.setVisibility(View.GONE);
-            }
-        }
-    }
-
-    @NonNull
-    @Override
-    public SectionedViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
-        if (viewType == VIEW_TYPE_HEADER) {
-            return new HeaderViewHolder(
-                SyncedFoldersItemHeaderBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)
-            );
-        } else if (viewType == VIEW_TYPE_FOOTER) {
-            return new FooterViewHolder(
-                SyncedFoldersFooterBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)
-            );
-        } else if (viewType == VIEW_TYPE_EMPTY) {
-            return new EmptyViewHolder(
-                SyncedFoldersEmptyBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)
-            );
-        } else {
-            return new MainViewHolder(
-                GridSyncItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)
-            );
-        }
-    }
-
-    private boolean isLastSection(int section) {
-        return section >= getSectionCount() - 1;
-    }
-
-    public int getHiddenFolderCount() {
-        if (syncFolderItems != null && filteredSyncFolderItems != null) {
-            return syncFolderItems.size() - filteredSyncFolderItems.size();
-        } else {
-            return 0;
-        }
-    }
-
-    public interface ClickListener {
-        void onSyncStatusToggleClick(int section, SyncedFolderDisplayItem syncedFolderDisplayItem);
-        void onSyncFolderSettingsClick(int section, SyncedFolderDisplayItem syncedFolderDisplayItem);
-        void onVisibilityToggleClick(int section, SyncedFolderDisplayItem item);
-    }
-
-    static class HeaderViewHolder extends SectionedViewHolder {
-        protected SyncedFoldersItemHeaderBinding binding;
-
-        private HeaderViewHolder(SyncedFoldersItemHeaderBinding binding) {
-            super(binding.getRoot());
-            this.binding = binding;
-        }
-    }
-
-    static class FooterViewHolder extends SectionedViewHolder {
-        protected SyncedFoldersFooterBinding binding;
-
-        private FooterViewHolder(SyncedFoldersFooterBinding binding) {
-            super(binding.getRoot());
-            this.binding = binding;
-        }
-    }
-
-    static class EmptyViewHolder extends SectionedViewHolder {
-        private EmptyViewHolder(SyncedFoldersEmptyBinding binding) {
-            super(binding.getRoot());
-        }
-    }
-
-    static class MainViewHolder extends SectionedViewHolder {
-        protected GridSyncItemBinding binding;
-
-        private MainViewHolder(GridSyncItemBinding binding) {
-            super(binding.getRoot());
-            this.binding = binding;
-        }
-    }
-
-    private void setSyncButtonActiveIcon(ImageButton syncStatusButton, boolean enabled) {
-        if (enabled) {
-            syncStatusButton.setImageDrawable(
-                viewThemeUtils.platform.tintPrimaryDrawable(context, R.drawable.ic_cloud_sync_on)
-                                             );
-        } else {
-            syncStatusButton.setImageResource(R.drawable.ic_cloud_sync_off);
-        }
-    }
-}

+ 456 - 0
app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.kt

@@ -0,0 +1,456 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2016 Andy Scherzinger
+ * SPDX-FileCopyrightText: 2016 Nextcloud
+ * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
+ */
+package com.owncloud.android.ui.adapter
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageButton
+import android.widget.PopupMenu
+import androidx.annotation.VisibleForTesting
+import com.afollestad.sectionedrecyclerview.SectionedRecyclerViewAdapter
+import com.afollestad.sectionedrecyclerview.SectionedViewHolder
+import com.nextcloud.android.common.ui.theme.utils.ColorRole
+import com.nextcloud.client.core.Clock
+import com.owncloud.android.R
+import com.owncloud.android.databinding.GridSyncItemBinding
+import com.owncloud.android.databinding.SyncedFoldersEmptyBinding
+import com.owncloud.android.databinding.SyncedFoldersFooterBinding
+import com.owncloud.android.databinding.SyncedFoldersItemHeaderBinding
+import com.owncloud.android.datamodel.MediaFolderType
+import com.owncloud.android.datamodel.SyncedFolderDisplayItem
+import com.owncloud.android.datamodel.ThumbnailsCacheManager
+import com.owncloud.android.datamodel.ThumbnailsCacheManager.AsyncMediaThumbnailDrawable
+import com.owncloud.android.datamodel.ThumbnailsCacheManager.MediaThumbnailGenerationTask
+import com.owncloud.android.utils.theme.ViewThemeUtils
+import java.io.File
+import java.util.Locale
+import java.util.concurrent.Executor
+import java.util.concurrent.Executors
+
+/**
+ * Adapter to display all auto-synced folders and/or instant upload media folders.
+ */
+@Suppress("LongParameterList")
+class SyncedFolderAdapter(
+    private val context: Context,
+    private val clock: Clock,
+    private val gridWidth: Int,
+    private val clickListener: ClickListener,
+    private val light: Boolean,
+    private val viewThemeUtils: ViewThemeUtils
+) : SectionedRecyclerViewAdapter<SectionedViewHolder>() {
+
+    private val gridTotal = gridWidth * 2
+    private val syncFolderItems: MutableList<SyncedFolderDisplayItem> = ArrayList()
+    private val filteredSyncFolderItems: MutableList<SyncedFolderDisplayItem> = ArrayList()
+    private var hideItems = true
+    private val thumbnailThreadPool: Executor = Executors.newCachedThreadPool()
+
+    init {
+        shouldShowHeadersForEmptySections(true)
+        shouldShowFooters(true)
+    }
+
+    @SuppressLint("NotifyDataSetChanged")
+    fun toggleHiddenItemsVisibility() {
+        hideItems = !hideItems
+
+        filterHiddenItems(syncFolderItems, hideItems)?.let {
+            filteredSyncFolderItems.clear()
+            filteredSyncFolderItems.addAll(it)
+            notifyDataSetChanged()
+        }
+    }
+
+    fun setSyncFolderItems(syncFolderItems: List<SyncedFolderDisplayItem>) {
+        this.syncFolderItems.clear()
+        this.syncFolderItems.addAll(syncFolderItems)
+
+        filterHiddenItems(this.syncFolderItems, hideItems)?.let {
+            filteredSyncFolderItems.clear()
+            filteredSyncFolderItems.addAll(it)
+        }
+    }
+
+    @SuppressLint("NotifyDataSetChanged")
+    fun setSyncFolderItem(location: Int, syncFolderItem: SyncedFolderDisplayItem) {
+        if (hideItems && syncFolderItem.isHidden && filteredSyncFolderItems.contains(syncFolderItem)) {
+            filteredSyncFolderItems.removeAt(location)
+        } else {
+            if (filteredSyncFolderItems.contains(syncFolderItem)) {
+                filteredSyncFolderItems[filteredSyncFolderItems.indexOf(syncFolderItem)] = syncFolderItem
+            } else {
+                filteredSyncFolderItems.add(syncFolderItem)
+            }
+        }
+
+        if (syncFolderItems.contains(syncFolderItem)) {
+            syncFolderItems[syncFolderItems.indexOf(syncFolderItem)] = syncFolderItem
+        } else {
+            syncFolderItems.add(syncFolderItem)
+        }
+
+        notifyDataSetChanged()
+    }
+
+    @SuppressLint("NotifyDataSetChanged")
+    fun addSyncFolderItem(syncFolderItem: SyncedFolderDisplayItem) {
+        syncFolderItems.add(syncFolderItem)
+
+        // add item for display when either all items should be shown (!hideItems)
+        // or if item should be shown (!.isHidden())
+        if (!hideItems || !syncFolderItem.isHidden) {
+            filteredSyncFolderItems.add(syncFolderItem)
+            notifyDataSetChanged()
+        }
+    }
+
+    @SuppressLint("NotifyDataSetChanged")
+    fun removeItem(section: Int) {
+        if (filteredSyncFolderItems.contains(syncFolderItems[section])) {
+            filteredSyncFolderItems.remove(syncFolderItems[section])
+            notifyDataSetChanged()
+        }
+
+        syncFolderItems.removeAt(section)
+    }
+
+    /**
+     * Filter for hidden items
+     *
+     * @param items Collection of items to filter
+     * @return Non-hidden items
+     */
+    private fun filterHiddenItems(
+        items: List<SyncedFolderDisplayItem>?,
+        hide: Boolean
+    ): List<SyncedFolderDisplayItem>? {
+        if (!hide) {
+            return items
+        } else {
+            val result: MutableList<SyncedFolderDisplayItem> = ArrayList()
+
+            for (item in items!!) {
+                if (!item.isHidden && !result.contains(item)) {
+                    result.add(item)
+                }
+            }
+
+            return result
+        }
+    }
+
+    override fun getSectionCount(): Int {
+        return if (filteredSyncFolderItems.size > 0) {
+            filteredSyncFolderItems.size + 1
+        } else {
+            0
+        }
+    }
+
+    @VisibleForTesting
+    fun clear() {
+        filteredSyncFolderItems.clear()
+        syncFolderItems.clear()
+    }
+
+    val unfilteredSectionCount: Int
+        get() = if (syncFolderItems.size > 0) {
+            syncFolderItems.size + 1
+        } else {
+            0
+        }
+
+    override fun getItemCount(section: Int): Int {
+        if (section < filteredSyncFolderItems.size) {
+            val filePaths = filteredSyncFolderItems[section].filePaths
+
+            return if (filePaths != null) {
+                filteredSyncFolderItems[section].filePaths.size
+            } else {
+                1
+            }
+        } else {
+            return 1
+        }
+    }
+
+    fun get(section: Int): SyncedFolderDisplayItem? {
+        return if (section in filteredSyncFolderItems.indices) {
+            filteredSyncFolderItems[section]
+        } else {
+            null
+        }
+    }
+
+    override fun getItemViewType(section: Int, relativePosition: Int, absolutePosition: Int): Int {
+        return if (isLastSection(section)) {
+            VIEW_TYPE_EMPTY
+        } else {
+            VIEW_TYPE_ITEM
+        }
+    }
+
+    override fun getHeaderViewType(section: Int): Int {
+        return if (isLastSection(section)) {
+            VIEW_TYPE_EMPTY
+        } else {
+            VIEW_TYPE_HEADER
+        }
+    }
+
+    override fun getFooterViewType(section: Int): Int {
+        return if (isLastSection(section) && showFooter()) {
+            VIEW_TYPE_FOOTER
+        } else {
+            // only show footer after last item and only if folders have been hidden
+            VIEW_TYPE_EMPTY
+        }
+    }
+
+    private fun showFooter(): Boolean {
+        return syncFolderItems.size > filteredSyncFolderItems.size
+    }
+
+    /**
+     * returns the section of a synced folder for the given local path and type.
+     *
+     * @param localPath the local path of the synced folder
+     * @param type      the of the synced folder
+     * @return the section index of the looked up synced folder, `-1` if not present
+     */
+    fun getSectionByLocalPathAndType(localPath: String?, type: Int): Int {
+        for (i in filteredSyncFolderItems.indices) {
+            if (filteredSyncFolderItems[i].localPath.equals(localPath, ignoreCase = true) &&
+                filteredSyncFolderItems[i].type.id == type
+            ) {
+                return i
+            }
+        }
+
+        return -1
+    }
+
+    override fun onBindHeaderViewHolder(commonHolder: SectionedViewHolder, section: Int, expanded: Boolean) {
+        if (section < filteredSyncFolderItems.size) {
+            val holder = commonHolder as HeaderViewHolder
+            holder.binding.headerContainer.visibility = View.VISIBLE
+
+            holder.binding.title.text = filteredSyncFolderItems[section].folderName
+
+            if (MediaFolderType.VIDEO == filteredSyncFolderItems[section].type) {
+                holder.binding.type.setImageResource(R.drawable.video_32dp)
+            } else if (MediaFolderType.IMAGE == filteredSyncFolderItems[section].type) {
+                holder.binding.type.setImageResource(R.drawable.image_32dp)
+            } else {
+                holder.binding.type.setImageResource(R.drawable.folder_star_32dp)
+            }
+
+            holder.binding.syncStatusButton.visibility = View.VISIBLE
+            holder.binding.syncStatusButton.tag = section
+            holder.binding.syncStatusButton.setOnClickListener {
+                filteredSyncFolderItems[section].setEnabled(
+                    !filteredSyncFolderItems[section].isEnabled,
+                    clock.currentTime
+                )
+                setSyncButtonActiveIcon(
+                    holder.binding.syncStatusButton,
+                    filteredSyncFolderItems[section].isEnabled
+                )
+                clickListener.onSyncStatusToggleClick(section, filteredSyncFolderItems[section])
+            }
+            setSyncButtonActiveIcon(holder.binding.syncStatusButton, filteredSyncFolderItems[section].isEnabled)
+
+            if (light) {
+                holder.binding.settingsButton.visibility = View.GONE
+            } else {
+                holder.binding.settingsButton.visibility = View.VISIBLE
+                holder.binding.settingsButton.tag = section
+                holder.binding.settingsButton.setOnClickListener { v: View ->
+                    onOverflowIconClicked(
+                        section,
+                        filteredSyncFolderItems[section],
+                        v
+                    )
+                }
+            }
+        }
+    }
+
+    private fun onOverflowIconClicked(section: Int, item: SyncedFolderDisplayItem, view: View) {
+        val popup = PopupMenu(context, view).apply {
+            inflate(R.menu.synced_folders_adapter)
+            setOnMenuItemClickListener { i: MenuItem -> optionsItemSelected(i, section, item) }
+            menu
+                .findItem(R.id.action_auto_upload_folder_toggle_visibility)
+                .setChecked(item.isHidden)
+        }
+
+        popup.show()
+    }
+
+    private fun optionsItemSelected(menuItem: MenuItem, section: Int, item: SyncedFolderDisplayItem): Boolean {
+        if (menuItem.itemId == R.id.action_auto_upload_folder_toggle_visibility) {
+            clickListener.onVisibilityToggleClick(section, item)
+        } else {
+            // default: R.id.action_create_custom_folder
+            clickListener.onSyncFolderSettingsClick(section, item)
+        }
+        return true
+    }
+
+    override fun onBindFooterViewHolder(holder: SectionedViewHolder, section: Int) {
+        if (isLastSection(section) && showFooter()) {
+            val footerHolder = holder as FooterViewHolder
+            footerHolder.binding.footerText.setOnClickListener { toggleHiddenItemsVisibility() }
+            footerHolder.binding.footerText.text = context.resources.getQuantityString(
+                R.plurals.synced_folders_show_hidden_folders,
+                hiddenFolderCount,
+                hiddenFolderCount
+            )
+        }
+    }
+
+    override fun onBindViewHolder(
+        commonHolder: SectionedViewHolder,
+        section: Int,
+        relativePosition: Int,
+        absolutePosition: Int
+    ) {
+        if (section < filteredSyncFolderItems.size && filteredSyncFolderItems[section].filePaths != null) {
+            val holder = commonHolder as MainViewHolder
+
+            val file = File(filteredSyncFolderItems[section].filePaths[relativePosition])
+
+            val task =
+                MediaThumbnailGenerationTask(
+                    holder.binding.thumbnail,
+                    context,
+                    viewThemeUtils
+                )
+
+            val asyncDrawable =
+                AsyncMediaThumbnailDrawable(
+                    context.resources,
+                    ThumbnailsCacheManager.mDefaultImg
+                )
+            holder.binding.thumbnail.setImageDrawable(asyncDrawable)
+
+            task.executeOnExecutor(thumbnailThreadPool, file)
+
+            // set proper tag
+            holder.binding.thumbnail.tag = file.hashCode()
+
+            holder.itemView.tag = relativePosition % gridWidth
+
+            if (filteredSyncFolderItems[section].numberOfFiles > gridTotal &&
+                relativePosition >= gridTotal - 1
+            ) {
+                holder.binding.counter.text = String.format(
+                    Locale.US,
+                    "%d",
+                    filteredSyncFolderItems[section].numberOfFiles - gridTotal
+                )
+                holder.binding.counterLayout.visibility = View.VISIBLE
+                holder.binding.thumbnailDarkener.visibility = View.VISIBLE
+            } else {
+                holder.binding.counterLayout.visibility = View.GONE
+                holder.binding.thumbnailDarkener.visibility = View.GONE
+            }
+        }
+    }
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SectionedViewHolder {
+        return when (viewType) {
+            VIEW_TYPE_HEADER -> {
+                HeaderViewHolder(
+                    SyncedFoldersItemHeaderBinding.inflate(
+                        LayoutInflater.from(parent.context),
+                        parent,
+                        false
+                    )
+                )
+            }
+            VIEW_TYPE_FOOTER -> {
+                FooterViewHolder(
+                    SyncedFoldersFooterBinding.inflate(
+                        LayoutInflater.from(parent.context),
+                        parent,
+                        false
+                    )
+                )
+            }
+            VIEW_TYPE_EMPTY -> {
+                EmptyViewHolder(
+                    SyncedFoldersEmptyBinding.inflate(
+                        LayoutInflater.from(parent.context),
+                        parent,
+                        false
+                    )
+                )
+            }
+            else -> {
+                MainViewHolder(
+                    GridSyncItemBinding.inflate(
+                        LayoutInflater.from(parent.context),
+                        parent,
+                        false
+                    )
+                )
+            }
+        }
+    }
+
+    private fun isLastSection(section: Int): Boolean {
+        return section >= sectionCount - 1
+    }
+
+    val hiddenFolderCount: Int
+        get() = syncFolderItems.size - filteredSyncFolderItems.size
+
+    interface ClickListener {
+        fun onSyncStatusToggleClick(section: Int, syncedFolderDisplayItem: SyncedFolderDisplayItem?)
+        fun onSyncFolderSettingsClick(section: Int, syncedFolderDisplayItem: SyncedFolderDisplayItem?)
+        fun onVisibilityToggleClick(section: Int, item: SyncedFolderDisplayItem?)
+    }
+
+    internal class HeaderViewHolder(var binding: SyncedFoldersItemHeaderBinding) : SectionedViewHolder(
+        binding.root
+    )
+
+    internal class FooterViewHolder(var binding: SyncedFoldersFooterBinding) : SectionedViewHolder(
+        binding.root
+    )
+
+    internal class EmptyViewHolder(binding: SyncedFoldersEmptyBinding) : SectionedViewHolder(binding.root)
+
+    internal class MainViewHolder(var binding: GridSyncItemBinding) : SectionedViewHolder(
+        binding.root
+    )
+
+    private fun setSyncButtonActiveIcon(syncStatusButton: ImageButton, enabled: Boolean) {
+        if (enabled) {
+            syncStatusButton.setImageDrawable(
+                viewThemeUtils.platform.tintDrawable(context, R.drawable.ic_cloud_sync_on, ColorRole.PRIMARY)
+            )
+        } else {
+            syncStatusButton.setImageResource(R.drawable.ic_cloud_sync_off)
+        }
+    }
+
+    companion object {
+        private const val VIEW_TYPE_EMPTY = Int.MAX_VALUE
+        private const val VIEW_TYPE_ITEM = 1
+        private const val VIEW_TYPE_HEADER = 2
+        private const val VIEW_TYPE_FOOTER = 3
+    }
+}

+ 1 - 1
scripts/analysis/lint-results.txt

@@ -1,2 +1,2 @@
 DO NOT TOUCH; GENERATED BY DRONE
 DO NOT TOUCH; GENERATED BY DRONE
-      <span class="mdl-layout-title">Lint Report: 3 errors and 68 warnings</span>
+      <span class="mdl-layout-title">Lint Report: 3 errors and 64 warnings</span>