diff --git a/app/src/main/java/com/owncloud/android/datamodel/GalleryItems.kt b/app/src/main/java/com/owncloud/android/datamodel/GalleryItems.kt new file mode 100644 index 0000000000..4baa14dc93 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/datamodel/GalleryItems.kt @@ -0,0 +1,25 @@ +/* + * + * Nextcloud Android client application + * + * @author Tobias Kaminsky + * Copyright (C) 2022 Tobias Kaminsky + * Copyright (C) 2022 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 . + */ + +package com.owncloud.android.datamodel + +data class GalleryItems(val date: Long, val files: List) diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/CommonOCFileListAdapterInterface.kt b/app/src/main/java/com/owncloud/android/ui/adapter/CommonOCFileListAdapterInterface.kt new file mode 100644 index 0000000000..35313769da --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/CommonOCFileListAdapterInterface.kt @@ -0,0 +1,52 @@ +/* + * + * Nextcloud Android client application + * + * @author Tobias Kaminsky + * Copyright (C) 2022 Tobias Kaminsky + * Copyright (C) 2022 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 . + */ + +package com.owncloud.android.ui.adapter + +import com.nextcloud.client.account.User +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.utils.FileSortOrder + +interface CommonOCFileListAdapterInterface { + fun isMultiSelect(): Boolean + fun cancelAllPendingTasks() + fun getItemPosition(file: OCFile): Int + fun swapDirectory( + user: User, + directory: OCFile, + storageManager: FileDataStorageManager, + onlyOnDevice: Boolean, + mLimitToMimeType: String + ) + + fun setHighlightedItem(file: OCFile) + fun setSortOrder(mFile: OCFile, sortOrder: FileSortOrder) + fun addCheckedFile(file: OCFile) + fun isCheckedFile(file: OCFile): Boolean + fun getCheckedItems(): Set + fun removeCheckedFile(file: OCFile) + fun notifyItemChanged(file: OCFile) + fun getFilesCount(): Int + fun setMultiSelect(boolean: Boolean) + fun clearCheckedItems() +} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/GalleryAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/GalleryAdapter.kt new file mode 100644 index 0000000000..dd72629c4f --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/GalleryAdapter.kt @@ -0,0 +1,249 @@ +/* + * + * Nextcloud Android client application + * + * @author Tobias Kaminsky + * Copyright (C) 2022 Tobias Kaminsky + * Copyright (C) 2022 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 . + */ + +package com.owncloud.android.ui.adapter + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.annotation.VisibleForTesting +import com.afollestad.sectionedrecyclerview.SectionedRecyclerViewAdapter +import com.afollestad.sectionedrecyclerview.SectionedViewHolder +import com.nextcloud.client.account.User +import com.nextcloud.client.preferences.AppPreferences +import com.owncloud.android.databinding.GalleryHeaderBinding +import com.owncloud.android.databinding.GridImageBinding +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.GalleryItems +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.ui.activity.ComponentsGetter +import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface +import com.owncloud.android.utils.DisplayUtils +import com.owncloud.android.utils.FileSortOrder +import com.owncloud.android.utils.FileStorageUtils +import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView.SectionedAdapter +import java.util.Calendar +import java.util.Date + +class GalleryAdapter( + val context: Context, + user: User, + ocFileListFragmentInterface: OCFileListFragmentInterface, + preferences: AppPreferences, + transferServiceGetter: ComponentsGetter +) : SectionedRecyclerViewAdapter(), CommonOCFileListAdapterInterface, SectionedAdapter { + private var files: List = mutableListOf() + private var ocFileListDelegate: OCFileListDelegate + private var storageManager: FileDataStorageManager + + init { + storageManager = transferServiceGetter.storageManager + + ocFileListDelegate = OCFileListDelegate( + context, + ocFileListFragmentInterface, + user, + storageManager, + false, + preferences, + true, + transferServiceGetter, + showMetadata = false, + showShareAvatar = false + ) + } + + override fun showFooters(): Boolean = false + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SectionedViewHolder { + return if (viewType == VIEW_TYPE_HEADER) { + GalleryHeaderViewHolder( + GalleryHeaderBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } else { + GalleryItemViewHolder( + GridImageBinding.inflate(LayoutInflater.from(parent.context), parent, false) + ) + } + } + + override fun onBindViewHolder( + holder: SectionedViewHolder?, + section: Int, + relativePosition: Int, + absolutePosition: Int + ) { + if (holder != null) { + val itemViewHolder = holder as GalleryItemViewHolder + val ocFile = files[section].files[relativePosition] + + ocFileListDelegate.bindGridViewHolder(itemViewHolder, ocFile) + } + } + + override fun getItemCount(section: Int): Int { + return files[section].files.size + } + + override fun getSectionCount(): Int { + return files.size + } + + override fun getSectionName(position: Int): String { + return DisplayUtils.getDateByPattern( + files[getRelativePosition(position).section()].date, + context, + DisplayUtils.MONTH_YEAR_PATTERN + ) + } + + override fun onBindHeaderViewHolder(holder: SectionedViewHolder?, section: Int, expanded: Boolean) { + if (holder != null) { + val headerViewHolder = holder as GalleryHeaderViewHolder + val galleryItem = files[section] + + headerViewHolder.binding.month.text = DisplayUtils.getDateByPattern( + galleryItem.date, + context, + DisplayUtils.MONTH_PATTERN + ) + headerViewHolder.binding.year.text = DisplayUtils.getDateByPattern( + galleryItem.date, + context, DisplayUtils.YEAR_PATTERN + ) + } + } + + override fun onBindFooterViewHolder(holder: SectionedViewHolder?, section: Int) { + TODO("Not yet implemented") + } + + @SuppressLint("NotifyDataSetChanged") + fun showAllGalleryItems(storageManager: FileDataStorageManager) { + val items = storageManager.allGalleryItems + + files = items + .groupBy { firstOfMonth(it.modificationTimestamp) } + .map { GalleryItems(it.key, FileStorageUtils.sortOcFolderDescDateModifiedWithoutFavoritesFirst(it.value)) } + .sortedBy { it.date }.reversed() + + Handler(Looper.getMainLooper()).post { notifyDataSetChanged() } + } + + private fun firstOfMonth(timestamp: Long): Long { + val cal = Calendar.getInstance() + cal.time = Date(timestamp) + cal.set(Calendar.DAY_OF_MONTH, cal.getActualMinimum(Calendar.DAY_OF_MONTH)) + cal.set(Calendar.HOUR_OF_DAY, 0) + cal.set(Calendar.MINUTE, 0) + cal.set(Calendar.SECOND, 0) + + return cal.timeInMillis + } + + fun isEmpty(): Boolean { + return files.isEmpty() + } + + fun getItem(position: Int): OCFile { + val itemCoord = getRelativePosition(position) + + return files[itemCoord.section()].files[itemCoord.relativePos()] + } + + override fun isMultiSelect(): Boolean { + return ocFileListDelegate.isMultiSelect + } + + override fun cancelAllPendingTasks() { + ocFileListDelegate.cancelAllPendingTasks() + } + + override fun getItemPosition(file: OCFile): Int { + val item = files.find { it.files.contains(file) } + return getAbsolutePosition(files.indexOf(item), item?.files?.indexOf(file) ?: 0) + } + + override fun swapDirectory( + user: User, + directory: OCFile, + storageManager: FileDataStorageManager, + onlyOnDevice: Boolean, + mLimitToMimeType: String + ) { + TODO("Not yet implemented") + } + + override fun setHighlightedItem(file: OCFile) { + TODO("Not yet implemented") + } + + override fun setSortOrder(mFile: OCFile, sortOrder: FileSortOrder) { + TODO("Not yet implemented") + } + + override fun addCheckedFile(file: OCFile) { + ocFileListDelegate.addCheckedFile(file) + } + + override fun isCheckedFile(file: OCFile): Boolean { + return ocFileListDelegate.isCheckedFile(file) + } + + override fun getCheckedItems(): Set { + return ocFileListDelegate.checkedItems + } + + override fun removeCheckedFile(file: OCFile) { + ocFileListDelegate.removeCheckedFile(file) + } + + override fun notifyItemChanged(file: OCFile) { + notifyItemChanged(getItemPosition(file)) + } + + override fun getFilesCount(): Int { + return files.fold(0) { acc, item -> acc + item.files.size } + } + + @SuppressLint("NotifyDataSetChanged") + override fun setMultiSelect(boolean: Boolean) { + ocFileListDelegate.isMultiSelect = boolean + notifyDataSetChanged() + } + + override fun clearCheckedItems() { + ocFileListDelegate.clearCheckedItems() + } + + @VisibleForTesting + fun addFiles(items: List) { + files = items + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/GalleryHeaderViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/GalleryHeaderViewHolder.kt new file mode 100644 index 0000000000..d781a4dc2e --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/GalleryHeaderViewHolder.kt @@ -0,0 +1,28 @@ +/* + * + * Nextcloud Android client application + * + * @author Tobias Kaminsky + * Copyright (C) 2022 Tobias Kaminsky + * Copyright (C) 2022 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 . + */ + +package com.owncloud.android.ui.adapter + +import com.afollestad.sectionedrecyclerview.SectionedViewHolder +import com.owncloud.android.databinding.GalleryHeaderBinding + +class GalleryHeaderViewHolder(val binding: GalleryHeaderBinding) : SectionedViewHolder(binding.root) diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/GalleryItemViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/GalleryItemViewHolder.kt new file mode 100644 index 0000000000..0cdb993d3a --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/GalleryItemViewHolder.kt @@ -0,0 +1,56 @@ +/* + * + * Nextcloud Android client application + * + * @author Tobias Kaminsky + * Copyright (C) 2022 Tobias Kaminsky + * Copyright (C) 2022 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 . + */ + +package com.owncloud.android.ui.adapter + +import android.view.View +import android.widget.ImageView +import com.afollestad.sectionedrecyclerview.SectionedViewHolder +import com.elyeproj.loaderviewlibrary.LoaderImageView +import com.owncloud.android.databinding.GridImageBinding + +class GalleryItemViewHolder(val binding: GridImageBinding) : + SectionedViewHolder(binding.root), ListGridImageViewHolder { + override val thumbnail: ImageView + get() = binding.thumbnail + + override val shimmerThumbnail: LoaderImageView + get() = binding.thumbnailShimmer + + override val favorite: ImageView + get() = binding.favoriteAction + + override val localFileIndicator: ImageView + get() = binding.localFileIndicator + + override val shared: ImageView + get() = binding.sharedIcon + + override val checkbox: ImageView + get() = binding.customCheckbox + + override val itemLayout: View + get() = binding.ListItemLayout + + override val unreadComments: ImageView + get() = binding.unreadComments +} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/ListGridImageViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/ListGridImageViewHolder.kt new file mode 100644 index 0000000000..fa36ef8664 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/ListGridImageViewHolder.kt @@ -0,0 +1,37 @@ +/* + * + * Nextcloud Android client application + * + * @author Tobias Kaminsky + * Copyright (C) 2022 Tobias Kaminsky + * Copyright (C) 2022 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 . + */ +package com.owncloud.android.ui.adapter + +import android.view.View +import android.widget.ImageView +import com.elyeproj.loaderviewlibrary.LoaderImageView + +interface ListGridImageViewHolder { + val thumbnail: ImageView + val shimmerThumbnail: LoaderImageView + val favorite: ImageView + val localFileIndicator: ImageView + val shared: ImageView + val checkbox: ImageView + val itemLayout: View + val unreadComments: ImageView +} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/ListGridItemViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/ListGridItemViewHolder.kt new file mode 100644 index 0000000000..b53e329ccc --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/ListGridItemViewHolder.kt @@ -0,0 +1,28 @@ +/* + * + * Nextcloud Android client application + * + * @author Tobias Kaminsky + * Copyright (C) 2022 Tobias Kaminsky + * Copyright (C) 2022 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 . + */ +package com.owncloud.android.ui.adapter + +import android.widget.TextView + +internal interface ListGridItemViewHolder : ListGridImageViewHolder { + val fileName: TextView +} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/ListItemViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/ListItemViewHolder.kt new file mode 100644 index 0000000000..e084283d86 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/ListItemViewHolder.kt @@ -0,0 +1,35 @@ +/* + * + * Nextcloud Android client application + * + * @author Tobias Kaminsky + * Copyright (C) 2022 Tobias Kaminsky + * Copyright (C) 2022 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 . + */ +package com.owncloud.android.ui.adapter + +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import com.owncloud.android.ui.AvatarGroupLayout + +internal interface ListItemViewHolder : ListGridItemViewHolder { + val fileSize: TextView + val fileSizeSeparator: View + val lastModification: TextView + val overflowMenu: ImageView + val sharedAvatars: AvatarGroupLayout +} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java index 33a271976e..8b36196780 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java @@ -25,12 +25,10 @@ package com.owncloud.android.ui.adapter; import android.accounts.AccountManager; +import android.annotation.SuppressLint; import android.app.Activity; import android.content.ContentValues; -import android.content.Context; import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.Point; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.os.Handler; @@ -39,11 +37,7 @@ import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.view.WindowManager; -import android.widget.Filter; -import android.widget.FrameLayout; import android.widget.ImageView; -import android.widget.TextView; import com.elyeproj.loaderviewlibrary.LoaderImageView; import com.nextcloud.client.account.User; @@ -60,8 +54,6 @@ import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.datamodel.ThumbnailsCacheManager; import com.owncloud.android.datamodel.VirtualFolderType; import com.owncloud.android.db.ProviderMeta; -import com.owncloud.android.files.services.FileDownloader; -import com.owncloud.android.files.services.FileUploader; import com.owncloud.android.lib.common.operations.RemoteOperation; import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.lib.resources.files.model.RemoteFile; @@ -70,20 +62,16 @@ import com.owncloud.android.lib.resources.shares.ShareType; import com.owncloud.android.lib.resources.shares.ShareeUser; import com.owncloud.android.operations.RefreshFolderOperation; import com.owncloud.android.operations.RemoteOperationFailedException; -import com.owncloud.android.services.OperationsService; -import com.owncloud.android.ui.AvatarGroupLayout; import com.owncloud.android.ui.activity.ComponentsGetter; import com.owncloud.android.ui.fragment.SearchType; import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface; import com.owncloud.android.ui.preview.PreviewTextFragment; -import com.owncloud.android.utils.BitmapUtils; import com.owncloud.android.utils.DisplayUtils; import com.owncloud.android.utils.FileSortOrder; import com.owncloud.android.utils.FileStorageUtils; import com.owncloud.android.utils.MimeTypeUtil; import com.owncloud.android.utils.theme.CapabilityUtils; import com.owncloud.android.utils.theme.ThemeColorUtils; -import com.owncloud.android.utils.theme.ThemeDrawableUtils; import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView; import java.io.File; @@ -91,26 +79,25 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; -import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; -import java.util.Vector; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import androidx.core.content.res.ResourcesCompat; import androidx.recyclerview.widget.RecyclerView; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** * This Adapter populates a RecyclerView with all files and folders in a Nextcloud instance. */ public class OCFileListAdapter extends RecyclerView.Adapter - implements DisplayUtils.AvatarGenerationListener, FastScrollRecyclerView.SectionedAdapter { + implements DisplayUtils.AvatarGenerationListener, + CommonOCFileListAdapterInterface, + FastScrollRecyclerView.SectionedAdapter { private static final int showFilenameColumnThreshold = 4; - private final ComponentsGetter transferServiceGetter; private final String userId; private Activity activity; private AppPreferences preferences; @@ -119,14 +106,11 @@ public class OCFileListAdapter extends RecyclerView.Adapter checkedFiles; private FileDataStorageManager mStorageManager; private User user; private OCFileListFragmentInterface ocFileListFragmentInterface; - private FilesFilter mFilesFilter; private OCFile currentDirectory; private static final String TAG = OCFileListAdapter.class.getSimpleName(); @@ -135,18 +119,15 @@ public class OCFileListAdapter extends RecyclerView.Adapter asyncTasks = new ArrayList<>(); private boolean onlyOnDevice; - private boolean showShareAvatar = false; - private OCFile highlightedItem; - private boolean showMetadata = true; + private final OCFileListDelegate ocFileListDelegate; private FileSortOrder sortOrder; private final SimpleDateFormat dateFormat = new SimpleDateFormat("MMMM yyyy", Locale.getDefault()); public OCFileListAdapter( Activity activity, - User user, + @NonNull User user, AppPreferences preferences, ComponentsGetter transferServiceGetter, OCFileListFragmentInterface ocFileListFragmentInterface, @@ -159,53 +140,54 @@ public class OCFileListAdapter extends RecyclerView.Adapter(); + mStorageManager = transferServiceGetter.getStorageManager(); - this.transferServiceGetter = transferServiceGetter; - - if (this.user != null) { - AccountManager platformAccountManager = AccountManager.get(this.activity); - userId = platformAccountManager.getUserData(this.user.toPlatformAccount(), - com.owncloud.android.lib.common.accounts.AccountUtils.Constants.KEY_USER_ID); - } else { - userId = ""; + if (mStorageManager == null) { + mStorageManager = new FileDataStorageManager(user, activity.getContentResolver()); } + userId = AccountManager + .get(activity) + .getUserData(this.user.toPlatformAccount(), + com.owncloud.android.lib.common.accounts.AccountUtils.Constants.KEY_USER_ID); + + ocFileListDelegate = new OCFileListDelegate(activity, + ocFileListFragmentInterface, + user, + mStorageManager, + hideItemOptions, + preferences, + gridView, + transferServiceGetter, + true, + CapabilityUtils + .getCapability(activity) + .getVersion() + .isShareesOnDavSupported()); + // initialise thumbnails cache on background thread new ThumbnailsCacheManager.InitDiskCacheTask().execute(); } public boolean isMultiSelect() { - return multiSelect; + return ocFileListDelegate.isMultiSelect(); } + @SuppressLint("NotifyDataSetChanged") public void setMultiSelect(boolean bool) { - multiSelect = bool; + ocFileListDelegate.setMultiSelect(bool); notifyDataSetChanged(); } - public boolean isCheckedFile(OCFile file) { - return checkedFiles.contains(file); - } - - public void removeCheckedFile(OCFile file) { - checkedFiles.remove(file); - } - - public void addCheckedFile(OCFile file) { - checkedFiles.add(file); - highlightedItem = null; + public void removeCheckedFile(@NonNull OCFile file) { + ocFileListDelegate.removeCheckedFile(file); } public void addAllFilesToCheckedFiles() { - checkedFiles.addAll(mFiles); + ocFileListDelegate.addToCheckedFiles(mFiles); } - public void removeAllFilesFromCheckedFiles() { - checkedFiles.clear(); - } - - public int getItemPosition(OCFile file) { + public int getItemPosition(@NonNull OCFile file) { int position = mFiles.indexOf(file); if (shouldShowHeader()) { @@ -357,23 +339,23 @@ public class OCFileListAdapter extends RecyclerView.Adapter ocFileListFragmentInterface.onHeaderClicked()); + PreviewTextFragment.setText(headerViewHolder.getHeaderText(), text, null, activity, true, true); + headerViewHolder.getHeaderView().setOnClickListener(v -> ocFileListFragmentInterface.onHeaderClicked()); } else { ListGridImageViewHolder gridViewHolder = (ListGridImageViewHolder) holder; - OCFile file = getItem(position); if (file == null) { @@ -381,311 +363,113 @@ public class OCFileListAdapter extends RecyclerView.Adapter ocFileListFragmentInterface.onItemClicked(file)); - - if (!hideItemOptions) { - gridViewHolder.getItemLayout().setLongClickable(true); - gridViewHolder.getItemLayout().setOnLongClickListener(v -> - ocFileListFragmentInterface.onLongItemClicked(file)); - } - - // unread comments - if (file.getUnreadCommentsCount() > 0) { - gridViewHolder.getUnreadComments().setVisibility(View.VISIBLE); - gridViewHolder.getUnreadComments().setOnClickListener(view -> ocFileListFragmentInterface - .showActivityDetailView(file)); - } else { - gridViewHolder.getUnreadComments().setVisibility(View.GONE); - } + ocFileListDelegate.bindGridViewHolder(gridViewHolder, file); if (holder instanceof ListItemViewHolder) { - ListItemViewHolder itemViewHolder = (ListItemViewHolder) holder; - - if ((file.isSharedWithMe() || file.isSharedWithSharee()) && !multiSelect && !gridView && - !hideItemOptions) { - itemViewHolder.getSharedAvatars().setVisibility(View.VISIBLE); - itemViewHolder.getSharedAvatars().removeAllViews(); - - String fileOwner = file.getOwnerId(); - List sharees = file.getSharees(); - - // use fileOwner if not oneself, then add at first - ShareeUser fileOwnerSharee = new ShareeUser(fileOwner, file.getOwnerDisplayName(), ShareType.USER); - if (!TextUtils.isEmpty(fileOwner) && - !fileOwner.equals(userId) && - !sharees.contains(fileOwnerSharee)) { - sharees.add(fileOwnerSharee); - } - - Collections.reverse(sharees); - - Log_OC.d(this, "sharees of " + file.getFileName() + ": " + sharees); - - itemViewHolder.getSharedAvatars().setAvatars(user, sharees); - itemViewHolder.getSharedAvatars().setOnClickListener( - view -> ocFileListFragmentInterface.onShareIconClick(file)); - } else { - itemViewHolder.getSharedAvatars().setVisibility(View.GONE); - itemViewHolder.getSharedAvatars().removeAllViews(); - } - - // npe fix: looks like file without local storage path somehow get here - final String storagePath = file.getStoragePath(); - if (onlyOnDevice && storagePath != null) { - File localFile = new File(storagePath); - long localSize; - if (localFile.isDirectory()) { - localSize = FileStorageUtils.getFolderSize(localFile); - } else { - localSize = localFile.length(); - } - - 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.getLastModification().setVisibility(View.GONE); - } - - - if (multiSelect || gridView || hideItemOptions) { - itemViewHolder.getOverflowMenu().setVisibility(View.GONE); - } else { - itemViewHolder.getOverflowMenu().setVisibility(View.VISIBLE); - itemViewHolder.getOverflowMenu().setOnClickListener(view -> ocFileListFragmentInterface - .onOverflowIconClicked(file, view)); - } - } - - gridViewHolder.getLocalFileIndicator().setVisibility(View.INVISIBLE); // default first - - if (showMetadata) { - OperationsService.OperationsServiceBinder operationsServiceBinder = transferServiceGetter.getOperationsServiceBinder(); - FileDownloader.FileDownloaderBinder fileDownloaderBinder = transferServiceGetter.getFileDownloaderBinder(); - FileUploader.FileUploaderBinder fileUploaderBinder = transferServiceGetter.getFileUploaderBinder(); - if (operationsServiceBinder != null && operationsServiceBinder.isSynchronizing(user, file)) { - //synchronizing - gridViewHolder.getLocalFileIndicator().setImageResource(R.drawable.ic_synchronizing); - gridViewHolder.getLocalFileIndicator().setVisibility(View.VISIBLE); - - } else if (fileDownloaderBinder != null && fileDownloaderBinder.isDownloading(user, file)) { - // downloading - gridViewHolder.getLocalFileIndicator().setImageResource(R.drawable.ic_synchronizing); - gridViewHolder.getLocalFileIndicator().setVisibility(View.VISIBLE); - - } else if (fileUploaderBinder != null && fileUploaderBinder.isUploading(user, file)) { - //uploading - gridViewHolder.getLocalFileIndicator().setImageResource(R.drawable.ic_synchronizing); - gridViewHolder.getLocalFileIndicator().setVisibility(View.VISIBLE); - - } else if (file.getEtagInConflict() != null) { - // conflict - gridViewHolder.getLocalFileIndicator().setImageResource(R.drawable.ic_synchronizing_error); - gridViewHolder.getLocalFileIndicator().setVisibility(View.VISIBLE); - - } else if (file.isDown()) { - gridViewHolder.getLocalFileIndicator().setImageResource(R.drawable.ic_synced); - gridViewHolder.getLocalFileIndicator().setVisibility(View.VISIBLE); - } - - gridViewHolder.getFavorite().setVisibility(file.isFavorite() ? View.VISIBLE : View.GONE); - } else { - gridViewHolder.getLocalFileIndicator().setVisibility(View.GONE); - gridViewHolder.getFavorite().setVisibility(View.GONE); - } - - if (multiSelect) { - gridViewHolder.getCheckbox().setVisibility(View.VISIBLE); - } else { - gridViewHolder.getCheckbox().setVisibility(View.GONE); + bindListItemViewHolder((ListItemViewHolder) gridViewHolder, file); } if (holder instanceof ListGridItemViewHolder) { - ListGridItemViewHolder gridItemViewHolder = (ListGridItemViewHolder) holder; - - gridItemViewHolder.getFileName().setText(file.getDecryptedFileName()); - - if (gridView && gridImage) { - gridItemViewHolder.getFileName().setVisibility(View.GONE); - } else { - if (gridView && ocFileListFragmentInterface.getColumnsCount() > showFilenameColumnThreshold) { - gridItemViewHolder.getFileName().setVisibility(View.GONE); - } else { - gridItemViewHolder.getFileName().setVisibility(View.VISIBLE); - } - } - } - - if (gridView || hideItemOptions || (file.isFolder() && !file.canReshare())) { - gridViewHolder.getShared().setVisibility(View.GONE); - } else { - showShareIcon(gridViewHolder, file); + bindListGridItemViewHolder((ListGridItemViewHolder) holder, file); } } } - public static void setThumbnail(OCFile file, - ImageView thumbnailView, - User user, - FileDataStorageManager storageManager, - List asyncTasks, - boolean gridView, - Context context) { - setThumbnail(file, thumbnailView, user, storageManager, asyncTasks, gridView, context, null, null); - } + private void bindListItemViewHolder(ListItemViewHolder holder, OCFile file) { + if ((file.isSharedWithMe() || file.isSharedWithSharee()) && !isMultiSelect() && !gridView && + !hideItemOptions) { + holder.getSharedAvatars().setVisibility(View.VISIBLE); + holder.getSharedAvatars().removeAllViews(); - private static void setThumbnail(OCFile file, - ImageView thumbnailView, - User user, - FileDataStorageManager storageManager, - List asyncTasks, - boolean gridView, - Context context, - LoaderImageView shimmerThumbnail, - AppPreferences preferences) { - if (file.isFolder()) { - stopShimmer(shimmerThumbnail, thumbnailView); - thumbnailView.setImageDrawable(MimeTypeUtil - .getFolderTypeIcon(file.isSharedWithMe() || file.isSharedWithSharee(), - file.isSharedViaLink(), file.isEncrypted(), - file.getMountType(), context)); + String fileOwner = file.getOwnerId(); + List sharees = file.getSharees(); + + // use fileOwner if not oneself, then add at first + ShareeUser fileOwnerSharee = new ShareeUser(fileOwner, file.getOwnerDisplayName(), ShareType.USER); + if (!TextUtils.isEmpty(fileOwner) && + !fileOwner.equals(userId) && + !sharees.contains(fileOwnerSharee)) { + sharees.add(fileOwnerSharee); + } + + Collections.reverse(sharees); + + Log_OC.d(this, "sharees of " + file.getFileName() + ": " + sharees); + + holder.getSharedAvatars().setAvatars(user, sharees); + holder.getSharedAvatars().setOnClickListener( + view -> ocFileListFragmentInterface.onShareIconClick(file)); } else { - if (file.getRemoteId() != null && file.isPreviewAvailable()) { - // Thumbnail in cache? - Bitmap thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache( - ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.getRemoteId() - ); + holder.getSharedAvatars().setVisibility(View.GONE); + holder.getSharedAvatars().removeAllViews(); + } - if (thumbnail != null && !file.isUpdateThumbnailNeeded()) { - stopShimmer(shimmerThumbnail, thumbnailView); - - if (MimeTypeUtil.isVideo(file)) { - Bitmap withOverlay = ThumbnailsCacheManager.addVideoOverlay(thumbnail); - thumbnailView.setImageBitmap(withOverlay); - } else { - if (gridView) { - BitmapUtils.setRoundedBitmapForGridMode(thumbnail, thumbnailView); - } else { - BitmapUtils.setRoundedBitmap(thumbnail, thumbnailView); - } - } - } else { - // generate new thumbnail - if (ThumbnailsCacheManager.cancelPotentialThumbnailWork(file, thumbnailView)) { - try { - final ThumbnailsCacheManager.ThumbnailGenerationTask task = - new ThumbnailsCacheManager.ThumbnailGenerationTask(thumbnailView, - storageManager, - user, - asyncTasks, - gridView); - if (thumbnail == null) { - Drawable drawable = MimeTypeUtil.getFileTypeIcon(file.getMimeType(), - file.getFileName(), - user, - context); - if (drawable == null) { - drawable = ResourcesCompat.getDrawable(context.getResources(), - R.drawable.file_image, - null); - } - int px = ThumbnailsCacheManager.getThumbnailDimension(); - thumbnail = BitmapUtils.drawableToBitmap(drawable, px, px); - } - final ThumbnailsCacheManager.AsyncThumbnailDrawable asyncDrawable = - new ThumbnailsCacheManager.AsyncThumbnailDrawable(context.getResources(), - thumbnail, task); - - if (shimmerThumbnail != null && shimmerThumbnail.getVisibility() == View.GONE) { - if (gridView) { - configShimmerGridImageSize(shimmerThumbnail, preferences.getGridColumns()); - } - startShimmer(shimmerThumbnail, thumbnailView); - } - - task.setListener(new ThumbnailsCacheManager.ThumbnailGenerationTask.Listener() { - @Override - public void onSuccess() { - stopShimmer(shimmerThumbnail, thumbnailView); - } - - @Override - public void onError() { - stopShimmer(shimmerThumbnail, thumbnailView); - } - }); - - thumbnailView.setImageDrawable(asyncDrawable); - asyncTasks.add(task); - task.execute(new ThumbnailsCacheManager.ThumbnailGenerationTaskObject(file, - file.getRemoteId())); - } catch (IllegalArgumentException e) { - Log_OC.d(TAG, "ThumbnailGenerationTask : " + e.getMessage()); - } - } - } - - if ("image/png".equalsIgnoreCase(file.getMimeType())) { - thumbnailView.setBackgroundColor(context.getResources().getColor(R.color.bg_default)); - } + // npe fix: looks like file without local storage path somehow get here + final String storagePath = file.getStoragePath(); + if (onlyOnDevice && storagePath != null) { + File localFile = new File(storagePath); + long localSize; + if (localFile.isDirectory()) { + localSize = FileStorageUtils.getFolderSize(localFile); } else { - stopShimmer(shimmerThumbnail, thumbnailView); - thumbnailView.setImageDrawable(MimeTypeUtil.getFileTypeIcon(file.getMimeType(), - file.getFileName(), - user, - context)); + localSize = localFile.length(); + } + + holder.getFileSize().setText(DisplayUtils.bytesToHumanReadable(localSize)); + holder.getFileSize().setVisibility(View.VISIBLE); + holder.getFileSizeSeparator().setVisibility(View.VISIBLE); + } else { + final long fileLength = file.getFileLength(); + if (fileLength >= 0) { + holder.getFileSize().setText(DisplayUtils.bytesToHumanReadable(fileLength)); + holder.getFileSize().setVisibility(View.VISIBLE); + holder.getFileSizeSeparator().setVisibility(View.VISIBLE); + } else { + holder.getFileSize().setVisibility(View.GONE); + holder.getFileSizeSeparator().setVisibility(View.GONE); + } + } + + final long modificationTimestamp = file.getModificationTimestamp(); + if (modificationTimestamp > 0) { + holder.getLastModification().setText(DisplayUtils.getRelativeTimestamp(activity, + modificationTimestamp)); + holder.getLastModification().setVisibility(View.VISIBLE); + } else if (file.getFirstShareTimestamp() > 0) { + holder.getLastModification().setText( + DisplayUtils.getRelativeTimestamp(activity, file.getFirstShareTimestamp()) + ); + holder.getLastModification().setVisibility(View.VISIBLE); + } else { + holder.getLastModification().setVisibility(View.GONE); + } + + + if (isMultiSelect() || gridView || hideItemOptions) { + holder.getOverflowMenu().setVisibility(View.GONE); + } else { + holder.getOverflowMenu().setVisibility(View.VISIBLE); + holder.getOverflowMenu().setOnClickListener(view -> ocFileListFragmentInterface + .onOverflowIconClicked(file, view)); + } + } + + private void bindListGridItemViewHolder(ListGridItemViewHolder holder, OCFile file) { + holder.getFileName().setText(file.getDecryptedFileName()); + + boolean gridImage = MimeTypeUtil.isImage(file) || MimeTypeUtil.isVideo(file); + if (gridView && gridImage) { + holder.getFileName().setVisibility(View.GONE); + } else { + if (gridView && ocFileListFragmentInterface.getColumnsCount() > showFilenameColumnThreshold) { + holder.getFileName().setVisibility(View.GONE); + } else { + holder.getFileName().setVisibility(View.VISIBLE); } } } + @Override public void onViewAttachedToWindow(@NonNull RecyclerView.ViewHolder holder) { if (holder instanceof ListGridImageViewHolder) { @@ -697,51 +481,6 @@ public class OCFileListAdapter extends RecyclerView.Adapter ocFileListFragmentInterface.onShareIconClick(file)); - } else { - sharedIconView.setVisibility(View.GONE); - } - } - /** * Change the adapted directory for a new one * @@ -874,17 +586,18 @@ public class OCFileListAdapter extends RecyclerView.Adapter getCheckedItems() { - return checkedFiles; + return ocFileListDelegate.getCheckedItems(); } public void setCheckedItem(Set files) { - checkedFiles.clear(); - checkedFiles.addAll(files); + ocFileListDelegate.setCheckedItem(files); } public void clearCheckedItems() { - checkedFiles.clear(); + ocFileListDelegate.clearCheckedItems(); + } + + public void setFiles(List files) { + mFiles = files; } public List getFiles() { return mFiles; } - public Filter getFilter() { - if (mFilesFilter == null) { - mFilesFilter = new FilesFilter(); - } - return mFilesFilter; - } - public void resetLastTimestamp() { lastTimestamp = -1; } @@ -1134,50 +833,16 @@ public class OCFileListAdapter extends RecyclerView.Adapter filteredFiles = new Vector<>(); + public void addCheckedFile(OCFile file) { + ocFileListDelegate.addCheckedFile(file); + } - if (!TextUtils.isEmpty(constraint)) { - for (OCFile file : mFilesAll) { - if (file.getParentRemotePath().equals(currentDirectory.getRemotePath()) && - file.getFileName().toLowerCase(Locale.getDefault()).contains( - constraint.toString().toLowerCase(Locale.getDefault())) && - !filteredFiles.contains(file)) { - filteredFiles.add(file); - } - } - } - - results.values = filteredFiles; - results.count = filteredFiles.size(); - - return results; - } - - @SuppressWarnings("unchecked") - @Override - protected void publishResults(CharSequence constraint, Filter.FilterResults results) { - - Vector ocFiles = (Vector) results.values; - mFiles.clear(); - if (ocFiles != null && ocFiles.size() > 0) { - mFiles.addAll(ocFiles); - if (!preferences.isShowHiddenFilesEnabled()) { - mFiles = filterHiddenFiles(mFiles); - } - sortOrder = preferences.getSortOrderByFolder(currentDirectory); - mFiles = sortOrder.sortCloudFiles(mFiles); - } - - notifyDataSetChanged(); - } + public void setHighlightedItem(OCFile file) { + ocFileListDelegate.setHighlightedItem(file); } /** @@ -1211,17 +876,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter getAllFiles() { + return mFilesAll; } - static class OCFileListGridItemViewHolder extends RecyclerView.ViewHolder implements ListGridItemViewHolder { - protected GridItemBinding binding; - - private OCFileListGridItemViewHolder(GridItemBinding binding) { - super(binding.getRoot()); - this.binding = binding; - this.binding.favoriteAction.getDrawable().mutate(); - } - - @Override - public TextView getFileName() { - return binding.Filename; - } - - @Override - public ImageView getThumbnail() { - return binding.thumbnail; - } - - @Override - public LoaderImageView getShimmerThumbnail() { - return binding.thumbnailShimmer; - } - - @Override - public ImageView getFavorite() { - return binding.favoriteAction; - } - - @Override - public ImageView getLocalFileIndicator() { - return binding.localFileIndicator; - } - - @Override - public ImageView getShared() { - return binding.sharedIcon; - } - - @Override - public ImageView getCheckbox() { - return binding.customCheckbox; - } - - @Override - public View getItemLayout() { - return binding.ListItemLayout; - } - - @Override - public ImageView getUnreadComments() { - return binding.unreadComments; - } + public OCFile getCurrentDirectory() { + return currentDirectory; } - static class OCFileListGridImageViewHolder extends RecyclerView.ViewHolder implements ListGridImageViewHolder { - protected GridImageBinding binding; - - private OCFileListGridImageViewHolder(GridImageBinding binding) { - super(binding.getRoot()); - this.binding = binding; - this.binding.favoriteAction.getDrawable().mutate(); - } - - @Override - public ImageView getThumbnail() { - return binding.thumbnail; - } - - @Override - public LoaderImageView getShimmerThumbnail() { - return binding.thumbnailShimmer; - } - - @Override - public ImageView getFavorite() { - return binding.favoriteAction; - } - - @Override - public ImageView getLocalFileIndicator() { - return binding.localFileIndicator; - } - - @Override - public ImageView getShared() { - return binding.sharedIcon; - } - - @Override - public ImageView getCheckbox() { - return binding.customCheckbox; - } - - @Override - public View getItemLayout() { - return binding.ListItemLayout; - } - - @Override - public ImageView getUnreadComments() { - return binding.unreadComments; - } + @Override + public int getFilesCount() { + return mFiles.size(); } - static class OCFileListFooterViewHolder extends RecyclerView.ViewHolder { - protected ListFooterBinding binding; - - private OCFileListFooterViewHolder(ListFooterBinding binding) { - super(binding.getRoot()); - this.binding = binding; - } - } - - static class OCFileListHeaderViewHolder extends RecyclerView.ViewHolder { - protected ListHeaderBinding binding; - - private OCFileListHeaderViewHolder(ListHeaderBinding binding) { - super(binding.getRoot()); - this.binding = binding; - } - } - - interface ListGridImageViewHolder { - ImageView getThumbnail(); - - LoaderImageView getShimmerThumbnail(); - - ImageView getFavorite(); - - ImageView getLocalFileIndicator(); - - ImageView getShared(); - - ImageView getCheckbox(); - - View getItemLayout(); - - ImageView getUnreadComments(); - } - - interface ListGridItemViewHolder extends ListGridImageViewHolder { - TextView getFileName(); - } - - interface ListItemViewHolder extends ListGridItemViewHolder { - TextView getFileSize(); - - View getFileSizeSeparator(); - - TextView getLastModification(); - - ImageView getOverflowMenu(); - - AvatarGroupLayout getSharedAvatars(); + @Override + public void notifyItemChanged(@NonNull OCFile file) { + notifyItemChanged(getItemPosition(file)); } } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt new file mode 100644 index 0000000000..476ee49242 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt @@ -0,0 +1,253 @@ +/* + * + * Nextcloud Android client application + * + * @author Tobias Kaminsky + * Copyright (C) 2022 Tobias Kaminsky + * Copyright (C) 2022 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 . + */ +package com.owncloud.android.ui.adapter + +import android.content.Context +import android.view.View +import com.nextcloud.client.account.User +import com.nextcloud.client.preferences.AppPreferences +import com.owncloud.android.R +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.datamodel.ThumbnailsCacheManager.ThumbnailGenerationTask +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.ui.activity.ComponentsGetter +import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface +import com.owncloud.android.utils.DisplayUtils +import com.owncloud.android.utils.theme.ThemeColorUtils +import com.owncloud.android.utils.theme.ThemeDrawableUtils + +@Suppress("LongParameterList", "TooManyFunctions") +class OCFileListDelegate( + private val context: Context, + private val ocFileListFragmentInterface: OCFileListFragmentInterface, + private val user: User, + private val storageManager: FileDataStorageManager, + private val hideItemOptions: Boolean, + private val preferences: AppPreferences, + private val gridView: Boolean, + private val transferServiceGetter: ComponentsGetter, + private val showMetadata: Boolean, + private var showShareAvatar: Boolean +) { + private val checkedFiles: MutableSet = HashSet() + private var highlightedItem: OCFile? = null + var isMultiSelect = false + private val asyncTasks: MutableList = ArrayList() + fun setHighlightedItem(highlightedItem: OCFile?) { + this.highlightedItem = highlightedItem + } + + fun isCheckedFile(file: OCFile): Boolean { + return checkedFiles.contains(file) + } + + fun addCheckedFile(file: OCFile) { + checkedFiles.add(file) + highlightedItem = null + } + + fun removeCheckedFile(file: OCFile) { + checkedFiles.remove(file) + } + + fun addToCheckedFiles(files: List?) { + checkedFiles.addAll(files!!) + } + + val checkedItems: Set + get() = checkedFiles + + fun setCheckedItem(files: Set?) { + checkedFiles.clear() + checkedFiles.addAll(files!!) + } + + fun clearCheckedItems() { + checkedFiles.clear() + } + + fun bindGridViewHolder(gridViewHolder: ListGridImageViewHolder, file: OCFile) { + // thumbnail + gridViewHolder.thumbnail.tag = file.fileId + DisplayUtils.setThumbnail( + file, + gridViewHolder.thumbnail, + user, + storageManager, + asyncTasks, + gridView, + context, + gridViewHolder.shimmerThumbnail, + preferences + ) + // item layout + click listeners + bindGridItemLayout(file, gridViewHolder) + + // unread comments + bindUnreadComments(file, gridViewHolder) + + // multiSelect (Checkbox) + if (isMultiSelect) { + gridViewHolder.checkbox.visibility = View.VISIBLE + } else { + gridViewHolder.checkbox.visibility = View.GONE + } + + // download state + gridViewHolder.localFileIndicator.visibility = View.INVISIBLE // default first + + // metadata (downloaded, favorite) + bindGridMetadataViews(file, gridViewHolder) + + // shares + val shouldHideShare = gridView || hideItemOptions || file.isFolder && !file.canReshare() + if (shouldHideShare) { + gridViewHolder.shared.visibility = View.GONE + } else { + showShareIcon(gridViewHolder, file) + } + } + + private fun bindUnreadComments(file: OCFile, gridViewHolder: ListGridImageViewHolder) { + if (file.unreadCommentsCount > 0) { + gridViewHolder.unreadComments.visibility = View.VISIBLE + gridViewHolder.unreadComments.setOnClickListener { + ocFileListFragmentInterface + .showActivityDetailView(file) + } + } else { + gridViewHolder.unreadComments.visibility = View.GONE + } + } + + private fun bindGridItemLayout(file: OCFile, gridViewHolder: ListGridImageViewHolder) { + if (highlightedItem != null && file.fileId == highlightedItem!!.fileId) { + gridViewHolder.itemLayout.setBackgroundColor( + context.resources + .getColor(R.color.selected_item_background) + ) + } else if (isCheckedFile(file)) { + gridViewHolder.itemLayout.setBackgroundColor( + context.resources + .getColor(R.color.selected_item_background) + ) + gridViewHolder.checkbox.setImageDrawable( + ThemeDrawableUtils.tintDrawable( + R.drawable.ic_checkbox_marked, + ThemeColorUtils.primaryColor(context) + ) + ) + } else { + gridViewHolder.itemLayout.setBackgroundColor(context.resources.getColor(R.color.bg_default)) + gridViewHolder.checkbox.setImageResource(R.drawable.ic_checkbox_blank_outline) + } + gridViewHolder.itemLayout.setOnClickListener { ocFileListFragmentInterface.onItemClicked(file) } + if (!hideItemOptions) { + gridViewHolder.itemLayout.isLongClickable = true + gridViewHolder.itemLayout.setOnLongClickListener { + ocFileListFragmentInterface.onLongItemClicked( + file + ) + } + } + } + + private fun bindGridMetadataViews(file: OCFile, gridViewHolder: ListGridImageViewHolder) { + if (showMetadata) { + showLocalFileIndicator(file, gridViewHolder) + gridViewHolder.favorite.visibility = if (file.isFavorite) View.VISIBLE else View.GONE + } else { + gridViewHolder.localFileIndicator.visibility = View.GONE + gridViewHolder.favorite.visibility = View.GONE + } + } + + private fun showLocalFileIndicator(file: OCFile, gridViewHolder: ListGridImageViewHolder) { + val operationsServiceBinder = transferServiceGetter.operationsServiceBinder + val fileDownloaderBinder = transferServiceGetter.fileDownloaderBinder + val fileUploaderBinder = transferServiceGetter.fileUploaderBinder + when { + operationsServiceBinder?.isSynchronizing(user, file) == true || + fileDownloaderBinder?.isDownloading(user, file) == true || + fileUploaderBinder?.isUploading(user, file) == true -> { + // synchronizing, downloading or uploading + gridViewHolder.localFileIndicator.setImageResource(R.drawable.ic_synchronizing) + gridViewHolder.localFileIndicator.visibility = View.VISIBLE + } + file.etagInConflict != null -> { + // conflict + gridViewHolder.localFileIndicator.setImageResource(R.drawable.ic_synchronizing_error) + gridViewHolder.localFileIndicator.visibility = View.VISIBLE + } + file.isDown -> { + // downloaded + gridViewHolder.localFileIndicator.setImageResource(R.drawable.ic_synced) + gridViewHolder.localFileIndicator.visibility = View.VISIBLE + } + } + } + + private fun showShareIcon(gridViewHolder: ListGridImageViewHolder, file: OCFile) { + val sharedIconView = gridViewHolder.shared + if (gridViewHolder is OCFileListItemViewHolder || file.unreadCommentsCount == 0) { + sharedIconView.visibility = View.VISIBLE + if (file.isSharedWithSharee || file.isSharedWithMe) { + if (showShareAvatar) { + sharedIconView.visibility = View.GONE + } else { + sharedIconView.visibility = View.VISIBLE + sharedIconView.setImageResource(R.drawable.shared_via_users) + sharedIconView.contentDescription = context.getString(R.string.shared_icon_shared) + } + } else if (file.isSharedViaLink) { + sharedIconView.setImageResource(R.drawable.shared_via_link) + sharedIconView.contentDescription = context.getString(R.string.shared_icon_shared_via_link) + } else { + sharedIconView.setImageResource(R.drawable.ic_unshared) + sharedIconView.contentDescription = context.getString(R.string.shared_icon_share) + } + sharedIconView.setOnClickListener { ocFileListFragmentInterface.onShareIconClick(file) } + } else { + sharedIconView.visibility = View.GONE + } + } + + fun cancelAllPendingTasks() { + for (task in asyncTasks) { + task.cancel(true) + if (task.getMethod != null) { + Log_OC.d(TAG, "cancel: abort get method directly") + task.getMethod.abort() + } + } + asyncTasks.clear() + } + + fun setShowShareAvatar(bool: Boolean) { + showShareAvatar = bool + } + + companion object { + private val TAG = OCFileListDelegate::class.java.simpleName + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListFooterViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListFooterViewHolder.kt new file mode 100644 index 0000000000..b319aff85a --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListFooterViewHolder.kt @@ -0,0 +1,35 @@ +/* + * + * Nextcloud Android client application + * + * @author Tobias Kaminsky + * Copyright (C) 2022 Tobias Kaminsky + * Copyright (C) 2022 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 . + */ +package com.owncloud.android.ui.adapter + +import androidx.recyclerview.widget.RecyclerView +import com.owncloud.android.databinding.ListFooterBinding + +internal class OCFileListFooterViewHolder(var binding: ListFooterBinding) : RecyclerView.ViewHolder( + binding.root +) { + val footerText + get() = binding.footerText + + val loadingProgressBar + get() = binding.loadingProgressBar +} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridImageViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridImageViewHolder.kt new file mode 100644 index 0000000000..42064aa275 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridImageViewHolder.kt @@ -0,0 +1,55 @@ +/* + * + * Nextcloud Android client application + * + * @author Tobias Kaminsky + * Copyright (C) 2022 Tobias Kaminsky + * Copyright (C) 2022 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 . + */ +package com.owncloud.android.ui.adapter + +import android.view.View +import android.widget.ImageView +import androidx.recyclerview.widget.RecyclerView +import com.elyeproj.loaderviewlibrary.LoaderImageView +import com.owncloud.android.databinding.GridImageBinding + +internal class OCFileListGridImageViewHolder(var binding: GridImageBinding) : + RecyclerView.ViewHolder( + binding.root + ), + ListGridImageViewHolder { + override val thumbnail: ImageView + get() = binding.thumbnail + override val shimmerThumbnail: LoaderImageView + get() = binding.thumbnailShimmer + override val favorite: ImageView + get() = binding.favoriteAction + override val localFileIndicator: ImageView + get() = binding.localFileIndicator + override val shared: ImageView + get() = binding.sharedIcon + override val checkbox: ImageView + get() = binding.customCheckbox + override val itemLayout: View + get() = binding.ListItemLayout + override val unreadComments: ImageView + get() = binding.unreadComments + + init { + binding.favoriteAction.drawable.mutate() + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridItemViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridItemViewHolder.kt new file mode 100644 index 0000000000..6e7c8faf59 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridItemViewHolder.kt @@ -0,0 +1,58 @@ +/* + * + * Nextcloud Android client application + * + * @author Tobias Kaminsky + * Copyright (C) 2022 Tobias Kaminsky + * Copyright (C) 2022 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 . + */ +package com.owncloud.android.ui.adapter + +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.elyeproj.loaderviewlibrary.LoaderImageView +import com.owncloud.android.databinding.GridItemBinding + +internal class OCFileListGridItemViewHolder(var binding: GridItemBinding) : + RecyclerView.ViewHolder( + binding.root + ), + ListGridItemViewHolder { + override val fileName: TextView + get() = binding.Filename + override val thumbnail: ImageView + get() = binding.thumbnail + override val shimmerThumbnail: LoaderImageView + get() = binding.thumbnailShimmer + override val favorite: ImageView + get() = binding.favoriteAction + override val localFileIndicator: ImageView + get() = binding.localFileIndicator + override val shared: ImageView + get() = binding.sharedIcon + override val checkbox: ImageView + get() = binding.customCheckbox + override val itemLayout: View + get() = binding.ListItemLayout + override val unreadComments: ImageView + get() = binding.unreadComments + + init { + binding.favoriteAction.drawable.mutate() + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListHeaderViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListHeaderViewHolder.kt new file mode 100644 index 0000000000..6ddc6320c5 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListHeaderViewHolder.kt @@ -0,0 +1,35 @@ +/* + * + * Nextcloud Android client application + * + * @author Tobias Kaminsky + * Copyright (C) 2022 Tobias Kaminsky + * Copyright (C) 2022 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 . + */ +package com.owncloud.android.ui.adapter + +import androidx.recyclerview.widget.RecyclerView +import com.owncloud.android.databinding.ListHeaderBinding + +internal class OCFileListHeaderViewHolder(var binding: ListHeaderBinding) : RecyclerView.ViewHolder( + binding.root +) { + val headerText + get() = binding.headerText + + val headerView + get() = binding.headerView +} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListItemViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListItemViewHolder.kt new file mode 100644 index 0000000000..70a725ea69 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListItemViewHolder.kt @@ -0,0 +1,69 @@ +/* + * + * Nextcloud Android client application + * + * @author Tobias Kaminsky + * Copyright (C) 2022 Tobias Kaminsky + * Copyright (C) 2022 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 . + */ +package com.owncloud.android.ui.adapter + +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.elyeproj.loaderviewlibrary.LoaderImageView +import com.owncloud.android.databinding.ListItemBinding +import com.owncloud.android.ui.AvatarGroupLayout + +internal class OCFileListItemViewHolder(private var binding: ListItemBinding) : + RecyclerView.ViewHolder( + binding.root + ), + ListItemViewHolder { + override val fileSize: TextView + get() = binding.fileSize + override val fileSizeSeparator: View + get() = binding.fileSeparator + override val lastModification: TextView + get() = binding.lastMod + override val overflowMenu: ImageView + get() = binding.overflowMenu + override val sharedAvatars: AvatarGroupLayout + get() = binding.sharedAvatars + override val fileName: TextView + get() = binding.Filename + override val thumbnail: ImageView + get() = binding.thumbnail + override val shimmerThumbnail: LoaderImageView + get() = binding.thumbnailShimmer + override val favorite: ImageView + get() = binding.favoriteAction + override val localFileIndicator: ImageView + get() = binding.localFileIndicator + override val shared: ImageView + get() = binding.sharedIcon + override val checkbox: ImageView + get() = binding.customCheckbox + override val itemLayout: View + get() = binding.ListItemLayout + override val unreadComments: ImageView + get() = binding.unreadComments + + init { + binding.favoriteAction.drawable.mutate() + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/asynctasks/GallerySearchTask.java b/app/src/main/java/com/owncloud/android/ui/asynctasks/GallerySearchTask.java index 9500922e6e..1d91f56073 100644 --- a/app/src/main/java/com/owncloud/android/ui/asynctasks/GallerySearchTask.java +++ b/app/src/main/java/com/owncloud/android/ui/asynctasks/GallerySearchTask.java @@ -100,7 +100,7 @@ public class GallerySearchTask extends AsyncTask { positiveButton.setEnabled(binding.newCheckbox.isChecked() || binding.existingCheckbox.isChecked()); diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/GalleryFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/GalleryFragment.java index 82a64cf6d7..9ac137c916 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/GalleryFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/GalleryFragment.java @@ -31,6 +31,8 @@ import com.owncloud.android.R; import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.ui.activity.FileDisplayActivity; +import com.owncloud.android.ui.adapter.CommonOCFileListAdapterInterface; +import com.owncloud.android.ui.adapter.GalleryAdapter; import com.owncloud.android.ui.asynctasks.GallerySearchTask; import com.owncloud.android.ui.events.ChangeMenuEvent; @@ -50,6 +52,7 @@ public class GalleryFragment extends OCFileListFragment { private long endDate; private long daySpan = 30; private int limit = 300; + private GalleryAdapter mAdapter; @Override public void onCreate(Bundle savedInstanceState) { @@ -87,18 +90,45 @@ public class GalleryFragment extends OCFileListFragment { @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - mAdapter.setShowMetadata(false); currentSearchType = SearchType.GALLERY_SEARCH; - switchToGridView(); - menuItemAddRemoveValue = MenuItemAddRemove.REMOVE_GRID_AND_SORT; requireActivity().invalidateOptionsMenu(); handleSearchEvent(); } + @Override + protected void setAdapter(Bundle args) { + mAdapter = new GalleryAdapter(requireContext(), + accountManager.getUser(), + this, + preferences, + mContainerActivity); + +// val spacing = resources.getDimensionPixelSize(R.dimen.media_grid_spacing) +// binding.list.addItemDecoration(MediaGridItemDecoration(spacing)) + setRecyclerViewAdapter(mAdapter); + + + GridLayoutManager layoutManager = new GridLayoutManager(getContext(), getColumnsCount()); +// ((GridLayoutManager) layoutManager).setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { +// @Override +// public int getSpanSize(int position) { +// if (position == getAdapter().getItemCount() - 1 || +// position == 0 && getAdapter().shouldShowHeader()) { +// return ((GridLayoutManager) layoutManager).getSpanCount(); +// } else { +// return 1; +// } +// } +// }); + + mAdapter.setLayoutManager(layoutManager); + getRecyclerView().setLayoutManager(layoutManager); + } + @Override public void onRefresh() { super.onRefresh(); @@ -106,6 +136,11 @@ public class GalleryFragment extends OCFileListFragment { handleSearchEvent(); } + @Override + public CommonOCFileListAdapterInterface getCommonAdapter() { + return mAdapter; + } + @Override public void onResume() { super.onResume(); @@ -161,7 +196,7 @@ public class GalleryFragment extends OCFileListFragment { setEmptyListMessage(SearchType.GALLERY_SEARCH); } - if (emptySearch && getAdapter().getItemCount() > 0) { + if (emptySearch && mAdapter.getItemCount() > 0) { Log_OC.d(this, "End gallery search"); return; } @@ -234,8 +269,7 @@ public class GalleryFragment extends OCFileListFragment { } } - @Override - public boolean isGalleryFragment() { - return true; + public void showAllGalleryItems() { + mAdapter.showAllGalleryItems(mContainerActivity.getStorageManager()); } } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java index dd5d2e5287..6788ba6375 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java @@ -78,6 +78,7 @@ import com.owncloud.android.ui.activity.FileDisplayActivity; import com.owncloud.android.ui.activity.FolderPickerActivity; import com.owncloud.android.ui.activity.OnEnforceableRefreshListener; import com.owncloud.android.ui.activity.UploadFilesActivity; +import com.owncloud.android.ui.adapter.CommonOCFileListAdapterInterface; import com.owncloud.android.ui.adapter.OCFileListAdapter; import com.owncloud.android.ui.dialog.ChooseRichDocumentsTemplateDialogFragment; import com.owncloud.android.ui.dialog.ChooseTemplateDialogFragment; @@ -189,7 +190,7 @@ public class OCFileListFragment extends ExtendedListFragment implements protected FileFragment.ContainerActivity mContainerActivity; protected OCFile mFile; - protected OCFileListAdapter mAdapter; + private OCFileListAdapter mAdapter; protected boolean mOnlyFoldersClickable; protected boolean mFileSelectable; @@ -326,7 +327,9 @@ public class OCFileListFragment extends ExtendedListFragment implements @Override public void onPause() { super.onPause(); - mAdapter.cancelAllPendingTasks(); + if (mAdapter != null) { + mAdapter.cancelAllPendingTasks(); + } if (getActivity() != null) { getActivity().getIntent().removeExtra(OCFileListFragment.SEARCH_EVENT); @@ -349,18 +352,8 @@ public class OCFileListFragment extends ExtendedListFragment implements mOnlyFoldersClickable = args != null && args.getBoolean(ARG_ONLY_FOLDERS_CLICKABLE, false); mFileSelectable = args != null && args.getBoolean(ARG_FILE_SELECTABLE, false); mLimitToMimeType = args != null ? args.getString(ARG_MIMETYPE, "") : ""; - boolean hideItemOptions = args != null && args.getBoolean(ARG_HIDE_ITEM_OPTIONS, false); - mAdapter = new OCFileListAdapter( - getActivity(), - accountManager.getUser(), - preferences, - mContainerActivity, - this, - hideItemOptions, - isGridViewPreferred(mFile) - ); - setRecyclerViewAdapter(mAdapter); + setAdapter(args); mHideFab = args != null && args.getBoolean(ARG_HIDE_FAB, false); @@ -406,6 +399,22 @@ public class OCFileListFragment extends ExtendedListFragment implements listDirectory(false, false); } + protected void setAdapter(Bundle args) { + boolean hideItemOptions = args != null && args.getBoolean(ARG_HIDE_ITEM_OPTIONS, false); + + mAdapter = new OCFileListAdapter( + getActivity(), + accountManager.getUser(), + preferences, + mContainerActivity, + this, + hideItemOptions, + isGridViewPreferred(mFile) + ); + + setRecyclerViewAdapter(mAdapter); + } + protected void prepareCurrentSearch(SearchEvent event) { if (isSearchEventSet(event)) { @@ -691,7 +700,7 @@ public class OCFileListFragment extends ExtendedListFragment implements // hide FAB in multi selection mode setFabVisible(false); - mAdapter.setMultiSelect(true); + getCommonAdapter().setMultiSelect(true); return true; } @@ -700,12 +709,12 @@ public class OCFileListFragment extends ExtendedListFragment implements */ @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - final int checkedCount = mAdapter.getCheckedItems().size(); - Set checkedFiles = mAdapter.getCheckedItems(); + Set checkedFiles = getCommonAdapter().getCheckedItems(); + final int checkedCount = checkedFiles.size(); String title = getResources().getQuantityString(R.plurals.items_selected_count, checkedCount, checkedCount); mode.setTitle(title); FileMenuFilter mf = new FileMenuFilter( - mAdapter.getFiles().size(), + getCommonAdapter().getFilesCount(), checkedFiles, mContainerActivity, getActivity(), @@ -728,7 +737,7 @@ public class OCFileListFragment extends ExtendedListFragment implements */ @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - Set checkedFiles = mAdapter.getCheckedItems(); + Set checkedFiles = getCommonAdapter().getCheckedItems(); return onFileActionChosen(item, checkedFiles); } @@ -738,14 +747,14 @@ public class OCFileListFragment extends ExtendedListFragment implements @Override public void onDestroyActionMode(ActionMode mode) { mActiveActionMode = null; - + // show FAB on multi selection mode exit if (!mHideFab && !searchFragment) { setFabVisible(true); } - mAdapter.setMultiSelect(false); - mAdapter.clearCheckedItems(); + getCommonAdapter().setMultiSelect(false); + getCommonAdapter().clearCheckedItems(); } public void storeStateIn(Bundle outState) { @@ -865,10 +874,10 @@ public class OCFileListFragment extends ExtendedListFragment implements * @param file The concerned OCFile by the selection/deselection */ private void toggleItemToCheckedList(OCFile file) { - if (getAdapter().isCheckedFile(file)) { - getAdapter().removeCheckedFile(file); + if (getCommonAdapter().isCheckedFile(file)) { + getCommonAdapter().removeCheckedFile(file); } else { - getAdapter().addCheckedFile(file); + getCommonAdapter().addCheckedFile(file); } updateActionModeFile(file); } @@ -881,7 +890,7 @@ public class OCFileListFragment extends ExtendedListFragment implements mIsActionModeNew = false; if (mActiveActionMode != null) { mActiveActionMode.invalidate(); - mAdapter.notifyItemChanged(getAdapter().getItemPosition(file)); + getCommonAdapter().notifyItemChanged(file); } } @@ -894,7 +903,7 @@ public class OCFileListFragment extends ExtendedListFragment implements toggleItemToCheckedList(file); } else { actionBarActivity.startActionMode(mMultiChoiceModeListener); - getAdapter().addCheckedFile(file); + getCommonAdapter().addCheckedFile(file); } updateActionModeFile(file); } @@ -904,11 +913,11 @@ public class OCFileListFragment extends ExtendedListFragment implements @Override public void onItemClicked(OCFile file) { - if (getAdapter().isMultiSelect()) { + if (getCommonAdapter().isMultiSelect()) { toggleItemToCheckedList(file); } else { if (file != null) { - int position = mAdapter.getItemPosition(file); + int position = getCommonAdapter().getItemPosition(file); if (file.isFolder()) { resetHeaderScrollingState(); @@ -1229,6 +1238,10 @@ public class OCFileListFragment extends ExtendedListFragment implements if (!directory.isFolder()) { Log_OC.w(TAG, "You see, that is not a directory -> " + directory); directory = storageManager.getFileById(directory.getParentId()); + + if (directory == null) { + return; // no files, wait for sync + } } if (searchView != null && !searchView.isIconified() && !fromSearch) { @@ -1379,6 +1392,10 @@ public class OCFileListFragment extends ExtendedListFragment implements getAdapter().notifyDataSetChanged(); } + public CommonOCFileListAdapterInterface getCommonAdapter() { + return mAdapter; + } + public OCFileListAdapter getAdapter() { return mAdapter; } @@ -1587,7 +1604,11 @@ public class OCFileListFragment extends ExtendedListFragment implements FileDataStorageManager storageManager = mContainerActivity.getStorageManager(); - int position = mAdapter.getItemPosition(storageManager.getFileByRemoteId(event.remoteId)); + OCFile file = storageManager.getFileByRemoteId(event.remoteId); + int position = -1; + if (file != null) { + position = mAdapter.getItemPosition(file); + } SetupEncryptionDialogFragment dialog = SetupEncryptionDialogFragment.newInstance(user, position); dialog.setTargetFragment(this, SetupEncryptionDialogFragment.SETUP_ENCRYPTION_REQUEST_CODE); dialog.show(getParentFragmentManager(), SetupEncryptionDialogFragment.SETUP_ENCRYPTION_DIALOG_TAG); @@ -1656,11 +1677,6 @@ public class OCFileListFragment extends ExtendedListFragment implements return searchFragment; } - @Override - public boolean isGalleryFragment() { - return false; - } - /** * De-/select all elements in the current list view. * @@ -1672,7 +1688,7 @@ public class OCFileListFragment extends ExtendedListFragment implements if (select) { ocFileListAdapter.addAllFilesToCheckedFiles(); } else { - ocFileListAdapter.removeAllFilesFromCheckedFiles(); + ocFileListAdapter.clearCheckedItems(); } for (int i = 0; i < mAdapter.getItemCount(); i++) { diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchAsyncTask.kt b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchAsyncTask.kt index ee83961390..e2bdb8aa4a 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchAsyncTask.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListSearchAsyncTask.kt @@ -69,7 +69,7 @@ class OCFileListSearchAsyncTask( if (remoteOperationResult.resultData.isNullOrEmpty()) { fragment.setEmptyView(event) } else { - fragment.mAdapter.setData( + fragment.adapter.setData( remoteOperationResult.resultData, fragment.currentSearchType, fileDataStorageManager, @@ -85,7 +85,7 @@ class OCFileListSearchAsyncTask( fragmentReference.get()?.let { fragment -> fragment.isLoading = false if (!isCancelled) { - fragment.mAdapter.notifyDataSetChanged() + fragment.adapter.notifyDataSetChanged() } } } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/SharedListFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/SharedListFragment.kt index 5cf91566cc..9d53afe3a0 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/SharedListFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/SharedListFragment.kt @@ -59,7 +59,7 @@ class SharedListFragment : OCFileListFragment(), Injectable { override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - mAdapter.setShowMetadata(false) + adapter.setShowMetadata(false) currentSearchType = SearchType.SHARED_FILTER searchEvent = SearchEvent("", SearchRemoteOperation.SearchType.SHARED_FILTER) menuItemAddRemoveValue = MenuItemAddRemove.REMOVE_GRID_AND_SORT diff --git a/app/src/main/java/com/owncloud/android/ui/interfaces/OCFileListFragmentInterface.java b/app/src/main/java/com/owncloud/android/ui/interfaces/OCFileListFragmentInterface.java index 3b35dc19bb..5b2fd546d5 100644 --- a/app/src/main/java/com/owncloud/android/ui/interfaces/OCFileListFragmentInterface.java +++ b/app/src/main/java/com/owncloud/android/ui/interfaces/OCFileListFragmentInterface.java @@ -48,8 +48,4 @@ public interface OCFileListFragmentInterface { boolean isLoading(); void onHeaderClicked(); - - boolean isSearchFragment(); - - boolean isGalleryFragment(); } diff --git a/app/src/main/java/com/owncloud/android/utils/DataHolderUtil.java b/app/src/main/java/com/owncloud/android/utils/DataHolderUtil.java index 0cf9eef818..24f0e71681 100644 --- a/app/src/main/java/com/owncloud/android/utils/DataHolderUtil.java +++ b/app/src/main/java/com/owncloud/android/utils/DataHolderUtil.java @@ -27,6 +27,8 @@ import java.security.SecureRandom; import java.util.HashMap; import java.util.Map; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + /** * Data holder utility to store & retrieve stuff */ @@ -39,6 +41,7 @@ public class DataHolderUtil { @SuppressLint("TrulyRandom") private SecureRandom random = new SecureRandom(); + @SuppressFBWarnings("MS_EXPOSE_REP") public static synchronized DataHolderUtil getInstance() { if (instance == null) { instance = new DataHolderUtil(); diff --git a/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java b/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java index 96e79871d8..c450212aba 100644 --- a/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java +++ b/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java @@ -46,6 +46,9 @@ import android.text.style.StyleSpan; import android.util.DisplayMetrics; import android.util.Log; import android.view.View; +import android.view.WindowManager; +import android.widget.FrameLayout; +import android.widget.ImageView; import android.widget.Toast; import com.bumptech.glide.GenericRequestBuilder; @@ -56,13 +59,16 @@ import com.bumptech.glide.load.resource.file.FileToStreamDecoder; import com.bumptech.glide.request.target.SimpleTarget; import com.bumptech.glide.request.target.Target; import com.caverock.androidsvg.SVG; +import com.elyeproj.loaderviewlibrary.LoaderImageView; import com.google.android.material.snackbar.Snackbar; import com.nextcloud.client.account.CurrentAccountProvider; import com.nextcloud.client.account.User; import com.nextcloud.client.network.ClientFactory; +import com.nextcloud.client.preferences.AppPreferences; import com.owncloud.android.MainApp; import com.owncloud.android.R; import com.owncloud.android.datamodel.ArbitraryDataProvider; +import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.datamodel.ThumbnailsCacheManager; import com.owncloud.android.lib.common.OwnCloudAccount; @@ -89,17 +95,19 @@ import java.math.BigDecimal; import java.net.IDN; import java.nio.charset.Charset; import java.text.DateFormat; +import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; +import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.TimeZone; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.appcompat.widget.AppCompatDrawableManager; import androidx.core.content.res.ResourcesCompat; -import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; @@ -130,6 +138,10 @@ public final class DisplayUtils { private static final double BYTE_SIZE_DIVIDER_DOUBLE = 1024.0; private static final int DATE_TIME_PARTS_SIZE = 2; + public static final String MONTH_YEAR_PATTERN = "MMMM yyyy"; + public static final String MONTH_PATTERN = "MMMM"; + public static final String YEAR_PATTERN = "yyyy"; + private static Map mimeType2HumanReadable; static { @@ -820,4 +832,178 @@ public final class DisplayUtils { return R.string.menu_item_sort_by_name_a_z; } } + + public static String getDateByPattern(long timestamp, Context context, String pattern) { + DateFormat df = new SimpleDateFormat(pattern, context.getResources().getConfiguration().locale); + df.setTimeZone(TimeZone.getTimeZone(TimeZone.getDefault().getID())); + + return df.format(timestamp); + } + + public static void setThumbnail(OCFile file, + ImageView thumbnailView, + User user, + FileDataStorageManager storageManager, + List asyncTasks, + boolean gridView, + Context context) { + setThumbnail(file, + thumbnailView, + user, + storageManager, + asyncTasks, + gridView, + context, + null, + null); + } + + public static void setThumbnail(OCFile file, + ImageView thumbnailView, + User user, + FileDataStorageManager storageManager, + List asyncTasks, + boolean gridView, + Context context, + LoaderImageView shimmerThumbnail, + AppPreferences preferences) { + if (file.isFolder()) { + stopShimmer(shimmerThumbnail, thumbnailView); + thumbnailView.setImageDrawable(MimeTypeUtil + .getFolderTypeIcon(file.isSharedWithMe() || file.isSharedWithSharee(), + file.isSharedViaLink(), file.isEncrypted(), + file.getMountType(), context)); + } else { + if (file.getRemoteId() != null && file.isPreviewAvailable()) { + // Thumbnail in cache? + Bitmap thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache( + ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.getRemoteId() + ); + + if (thumbnail != null && !file.isUpdateThumbnailNeeded()) { + stopShimmer(shimmerThumbnail, thumbnailView); + + if (MimeTypeUtil.isVideo(file)) { + Bitmap withOverlay = ThumbnailsCacheManager.addVideoOverlay(thumbnail); + thumbnailView.setImageBitmap(withOverlay); + } else { + if (gridView) { + BitmapUtils.setRoundedBitmapForGridMode(thumbnail, thumbnailView); + } else { + BitmapUtils.setRoundedBitmap(thumbnail, thumbnailView); + } + } + } else { + // generate new thumbnail + if (ThumbnailsCacheManager.cancelPotentialThumbnailWork(file, thumbnailView)) { + try { + final ThumbnailsCacheManager.ThumbnailGenerationTask task = + new ThumbnailsCacheManager.ThumbnailGenerationTask(thumbnailView, + storageManager, + user, + asyncTasks, + gridView); + if (thumbnail == null) { + Drawable drawable = MimeTypeUtil.getFileTypeIcon(file.getMimeType(), + file.getFileName(), + user, + context); + if (drawable == null) { + drawable = ResourcesCompat.getDrawable(context.getResources(), + R.drawable.file_image, + null); + } + int px = ThumbnailsCacheManager.getThumbnailDimension(); + thumbnail = BitmapUtils.drawableToBitmap(drawable, px, px); + } + final ThumbnailsCacheManager.AsyncThumbnailDrawable asyncDrawable = + new ThumbnailsCacheManager.AsyncThumbnailDrawable(context.getResources(), + thumbnail, task); + + if (shimmerThumbnail != null && shimmerThumbnail.getVisibility() == View.GONE) { + if (gridView) { + configShimmerGridImageSize(shimmerThumbnail, preferences.getGridColumns()); + } + startShimmer(shimmerThumbnail, thumbnailView); + } + + task.setListener(new ThumbnailsCacheManager.ThumbnailGenerationTask.Listener() { + @Override + public void onSuccess() { + stopShimmer(shimmerThumbnail, thumbnailView); + } + + @Override + public void onError() { + stopShimmer(shimmerThumbnail, thumbnailView); + } + }); + + thumbnailView.setImageDrawable(asyncDrawable); + asyncTasks.add(task); + task.execute(new ThumbnailsCacheManager.ThumbnailGenerationTaskObject(file, + file.getRemoteId())); + } catch (IllegalArgumentException e) { + Log_OC.d(TAG, "ThumbnailGenerationTask : " + e.getMessage()); + } + } + } + + if ("image/png".equalsIgnoreCase(file.getMimeType())) { + thumbnailView.setBackgroundColor(context.getResources().getColor(R.color.bg_default)); + } + } else { + stopShimmer(shimmerThumbnail, thumbnailView); + thumbnailView.setImageDrawable(MimeTypeUtil.getFileTypeIcon(file.getMimeType(), + file.getFileName(), + user, + context)); + } + } + } + + private static void startShimmer(LoaderImageView thumbnailShimmer, ImageView thumbnailView) { + thumbnailShimmer.setImageResource(R.drawable.background); + thumbnailShimmer.resetLoader(); + thumbnailView.setVisibility(View.GONE); + thumbnailShimmer.setVisibility(View.VISIBLE); + } + + private static void stopShimmer(@Nullable LoaderImageView thumbnailShimmer, ImageView thumbnailView) { + if (thumbnailShimmer != null) { + thumbnailShimmer.setVisibility(View.GONE); + } + + thumbnailView.setVisibility(View.VISIBLE); + } + + private static void configShimmerGridImageSize(LoaderImageView thumbnailShimmer, float gridColumns) { + FrameLayout.LayoutParams targetLayoutParams = (FrameLayout.LayoutParams) thumbnailShimmer.getLayoutParams(); + + try { + final Point screenSize = getScreenSize(thumbnailShimmer.getContext()); + final int marginLeftAndRight = targetLayoutParams.leftMargin + targetLayoutParams.rightMargin; + final int size = Math.round(screenSize.x / gridColumns - marginLeftAndRight); + + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(size, size); + params.setMargins(targetLayoutParams.leftMargin, + targetLayoutParams.topMargin, + targetLayoutParams.rightMargin, + targetLayoutParams.bottomMargin); + thumbnailShimmer.setLayoutParams(params); + } catch (Exception exception) { + Log_OC.e("ConfigShimmer", exception.getMessage()); + } + } + + private static Point getScreenSize(Context context) throws Exception { + final WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + if (windowManager != null) { + final Point displaySize = new Point(); + windowManager.getDefaultDisplay().getSize(displaySize); + return displaySize; + } else { + throw new Exception("WindowManager not found"); + } + } } diff --git a/app/src/main/res/layout/gallery_header.xml b/app/src/main/res/layout/gallery_header.xml new file mode 100644 index 0000000000..1d21ae25b4 --- /dev/null +++ b/app/src/main/res/layout/gallery_header.xml @@ -0,0 +1,48 @@ + + + + + + + + diff --git a/app/src/test/java/com/owncloud/android/ui/adapter/GalleryAdapterTest.kt b/app/src/test/java/com/owncloud/android/ui/adapter/GalleryAdapterTest.kt new file mode 100644 index 0000000000..0e9cb9a12a --- /dev/null +++ b/app/src/test/java/com/owncloud/android/ui/adapter/GalleryAdapterTest.kt @@ -0,0 +1,94 @@ +/* + * + * Nextcloud Android client application + * + * @author Tobias Kaminsky + * Copyright (C) 2022 Tobias Kaminsky + * Copyright (C) 2022 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 . + */ + +package com.owncloud.android.ui.adapter + +import android.content.Context +import com.nextcloud.client.account.User +import com.nextcloud.client.preferences.AppPreferences +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.GalleryItems +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.ui.activity.ComponentsGetter +import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface +import junit.framework.Assert.assertEquals +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.whenever + +class GalleryAdapterTest { + @Mock + lateinit var context: Context + + @Mock + lateinit var user: User + + @Mock + lateinit var ocFileListFragmentInterface: OCFileListFragmentInterface + + @Mock + lateinit var preferences: AppPreferences + + @Mock + lateinit var transferServiceGetter: ComponentsGetter + + @Mock + lateinit var storageManager: FileDataStorageManager + + private lateinit var mocks: AutoCloseable + + @Before + fun setUp() { + mocks = MockitoAnnotations.openMocks(this) + } + + @After + fun tearDown() { + mocks.close() + } + + @Test + fun testItemCount() { + whenever(transferServiceGetter.storageManager) doReturn storageManager + + val sut = GalleryAdapter( + context, + user, + ocFileListFragmentInterface, + preferences, + transferServiceGetter + ) + + val list = listOf( + GalleryItems(1649317247, listOf(OCFile("/1.md"), OCFile("/2.md"))), + GalleryItems(1649317247, listOf(OCFile("/1.md"), OCFile("/2.md"))) + ) + + sut.addFiles(list) + + assertEquals(4, sut.getFilesCount()) + } +} diff --git a/scripts/analysis/findbugs-results.txt b/scripts/analysis/findbugs-results.txt index 7cbee132c8..db3a73389d 100644 --- a/scripts/analysis/findbugs-results.txt +++ b/scripts/analysis/findbugs-results.txt @@ -1 +1 @@ -592 +536 \ No newline at end of file diff --git a/scripts/analysis/lint-results.txt b/scripts/analysis/lint-results.txt index 2d857a44dc..a0c1e5f3ef 100644 --- a/scripts/analysis/lint-results.txt +++ b/scripts/analysis/lint-results.txt @@ -1,2 +1,2 @@ DO NOT TOUCH; GENERATED BY DRONE - Lint Report: 92 warnings + Lint Report: 89 warnings diff --git a/spotbugs-filter.xml b/spotbugs-filter.xml index ad1844bbe2..0f19982980 100644 --- a/spotbugs-filter.xml +++ b/spotbugs-filter.xml @@ -56,6 +56,7 @@ +