From 3e51d8be2827b78ab1bc0ecbbcdea291594d4602 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 29 Jul 2026 17:01:42 +0200 Subject: [PATCH 1/2] Rename .java to .kt Signed-off-by: alperozturk96 --- ...stFragmentInterface.java => LocalFileListFragmentInterface.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/com/owncloud/android/ui/interfaces/{LocalFileListFragmentInterface.java => LocalFileListFragmentInterface.kt} (100%) diff --git a/app/src/main/java/com/owncloud/android/ui/interfaces/LocalFileListFragmentInterface.java b/app/src/main/java/com/owncloud/android/ui/interfaces/LocalFileListFragmentInterface.kt similarity index 100% rename from app/src/main/java/com/owncloud/android/ui/interfaces/LocalFileListFragmentInterface.java rename to app/src/main/java/com/owncloud/android/ui/interfaces/LocalFileListFragmentInterface.kt From 105daab01f2ec4b31b09a57ed8506317ccffa089 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 29 Jul 2026 17:01:43 +0200 Subject: [PATCH 2/2] convert java to kotlin Signed-off-by: alperozturk96 --- .../ui/adapter/LocalFileListAdapter.java | 616 ------------------ .../localFileList/LocalFileListAdapter.kt | 327 ++++++++++ .../LocalFileListFooterViewHolder.kt | 16 + .../LocalFileListGridItemViewHolder.kt | 27 + .../localFileList/LocalFileListItemBinder.kt | 84 +++ .../LocalFileListItemViewHolder.kt | 23 + .../localFileList/LocalFileThumbnailBinder.kt | 74 +++ .../ui/dialog/ConflictsResolveDialog.kt | 13 +- .../ui/fragment/ExtendedListFragment.kt | 2 +- .../ui/fragment/LocalFileListFragment.java | 2 +- .../LocalFileListFragmentInterface.kt | 25 +- .../ui/adapter/LocalFileListAdapterTest.kt | 49 -- .../localFileList/LocalFileListAdapterTest.kt | 110 ++++ 13 files changed, 680 insertions(+), 688 deletions(-) delete mode 100644 app/src/main/java/com/owncloud/android/ui/adapter/LocalFileListAdapter.java create mode 100644 app/src/main/java/com/owncloud/android/ui/adapter/localFileList/LocalFileListAdapter.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/adapter/localFileList/LocalFileListFooterViewHolder.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/adapter/localFileList/LocalFileListGridItemViewHolder.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/adapter/localFileList/LocalFileListItemBinder.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/adapter/localFileList/LocalFileListItemViewHolder.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/adapter/localFileList/LocalFileThumbnailBinder.kt delete mode 100644 app/src/test/java/com/owncloud/android/ui/adapter/LocalFileListAdapterTest.kt create mode 100644 app/src/test/java/com/owncloud/android/ui/adapter/localFileList/LocalFileListAdapterTest.kt diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/LocalFileListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/LocalFileListAdapter.java deleted file mode 100644 index 72208f34f20c..000000000000 --- a/app/src/main/java/com/owncloud/android/ui/adapter/LocalFileListAdapter.java +++ /dev/null @@ -1,616 +0,0 @@ -/* - * Nextcloud Android client application - * - * @author Tobias Kaminsky - * Copyright (C) 2018 Tobias Kaminsky - * Copyright (C) 2018 Nextcloud - * - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.owncloud.android.ui.adapter; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.os.Handler; -import android.os.Looper; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; - -import com.nextcloud.android.common.ui.theme.utils.ColorRole; -import com.nextcloud.client.preferences.AppPreferences; -import com.nextcloud.utils.FileHelper; -import com.owncloud.android.R; -import com.owncloud.android.datamodel.ThumbnailsCacheManager; -import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.ui.adapter.storagePermissionBanner.StoragePermissionBannerViewHolder; -import com.owncloud.android.ui.interfaces.LocalFileListFragmentInterface; -import com.owncloud.android.utils.DisplayUtils; -import com.owncloud.android.utils.FileSortOrder; -import com.owncloud.android.utils.MimeTypeUtil; -import com.owncloud.android.utils.PermissionUtil; -import com.owncloud.android.utils.theme.ViewThemeUtils; - -import java.io.File; -import java.util.ArrayList; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Locale; -import java.util.Set; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; -import androidx.core.content.ContextCompat; -import androidx.recyclerview.widget.RecyclerView; - -/** - * This Adapter populates a {@link RecyclerView} with all files and directories contained in a local directory - */ -public class LocalFileListAdapter extends RecyclerView.Adapter implements FilterableListAdapter { - - private static final String TAG = LocalFileListAdapter.class.getSimpleName(); - - private final AppPreferences preferences; - private final Activity mContext; - private List mFiles = new ArrayList<>(); - private List mFilesAll = new ArrayList<>(); - private boolean mLocalFolderPicker; - private boolean gridView = false; - private LocalFileListFragmentInterface localFileListFragmentInterface; - private Set checkedFiles; - private ViewThemeUtils viewThemeUtils; - private boolean isWithinEncryptedFolder; - private final ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); - - private static final int VIEWTYPE_ITEM = 0; - private static final int VIEWTYPE_FOOTER = 1; - private static final int VIEWTYPE_IMAGE = 2; - private static final int VIEWTYPE_HEADER = 3; - - private static final int PAGE_SIZE = 50; - private int currentOffset = 0; - - private static final long HEADER_ID = Long.MIN_VALUE; - private static final long FOOTER_ID = Long.MIN_VALUE + 1; - - public LocalFileListAdapter(boolean localFolderPickerMode, - LocalFileListFragmentInterface localFileListFragmentInterface, - AppPreferences preferences, - Activity context, - final ViewThemeUtils viewThemeUtils, - boolean isWithinEncryptedFolder) { - this.preferences = preferences; - mContext = context; - mLocalFolderPicker = localFolderPickerMode; - this.localFileListFragmentInterface = localFileListFragmentInterface; - checkedFiles = new LinkedHashSet<>(); - this.viewThemeUtils = viewThemeUtils; - this.isWithinEncryptedFolder = isWithinEncryptedFolder; - setHasStableIds(true); - } - - public int getFilesCount() { - return mFiles.size(); - } - - public boolean isCheckedFile(File file) { - return checkedFiles.contains(file); - } - - public void removeCheckedFile(File file) { - checkedFiles.remove(file); - } - - public void addCheckedFile(File file) { - checkedFiles.add(file); - } - - public void addAllFilesToCheckedFiles() { - if (isWithinEncryptedFolder) { - for (File file : mFilesAll) { - if (file.isFile()) { - checkedFiles.add(file); - } - } - } else { - checkedFiles.addAll(mFiles); - } - } - - public void removeAllFilesFromCheckedFiles() { - checkedFiles.clear(); - } - - public int getItemPosition(File file) { - return mFiles.indexOf(file); - } - - public String[] getCheckedFilesPath() { - List result = FileHelper.INSTANCE.listFilesRecursive(checkedFiles); - - Log_OC.d(TAG, "Returning " + result.size() + " selected files"); - - return result.toArray(new String[0]); - } - - @Override - public int getItemCount() { - // footer always exists - int count = mFiles.size() + 1; - - // check header section visibility - if (shouldShowHeader()) { - count += 1; - } - - return count; - } - - @Override - public long getItemId(int position) { - int headerOffset = shouldShowHeader() ? 1 : 0; - return getStableItemId(position, headerOffset, mFiles); - } - - @VisibleForTesting - static long getStableItemId(int position, int headerOffset, List files) { - if (headerOffset == 1 && position == 0) { - return HEADER_ID; - } - - int fileIndex = position - headerOffset; - if (fileIndex < 0 || fileIndex >= files.size()) { - return FOOTER_ID; - } - - return files.get(fileIndex).getAbsolutePath().hashCode(); - } - - @Override - public int getItemViewType(int position) { - boolean header = shouldShowHeader(); - int headerOffset = header ? 1 : 0; - - if (header && position == 0) { - return VIEWTYPE_HEADER; - } - - // footer position - if (position == mFiles.size() + headerOffset) { - return VIEWTYPE_FOOTER; - } - - // real file position - int fileIndex = position - headerOffset; - File file = mFiles.get(fileIndex); - - if (MimeTypeUtil.isImageOrVideo(file)) { - return VIEWTYPE_IMAGE; - } else { - return VIEWTYPE_ITEM; - } - } - - private boolean shouldShowHeader() { - return !PermissionUtil.checkStoragePermission(mContext); - } - - @Override - public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { - boolean headerVisible = shouldShowHeader(); - int headerOffset = headerVisible ? 1 : 0; - - // --- HEADER --- - if (headerVisible && position == 0) { - // Header has no dynamic binding - return; - } - - // --- FOOTER --- - if (position == mFiles.size() + headerOffset) { - LocalFileListFooterViewHolder footer = (LocalFileListFooterViewHolder) holder; - footer.footerText.setText(getFooterText()); - return; - } - - int fileIndex = position - headerOffset; - if (fileIndex < 0 || fileIndex >= mFiles.size()) { - return; - } - - File file = mFiles.get(fileIndex); - if (file == null) { - return; - } - - LocalFileListGridItemViewHolder grid = (LocalFileListGridItemViewHolder) holder; - - // Background + checkbox logic - if (mLocalFolderPicker) { - grid.itemLayout.setBackgroundColor(mContext.getResources().getColor(R.color.bg_default)); - grid.checkbox.setVisibility(View.GONE); - } else { - grid.checkbox.setVisibility(View.VISIBLE); - - if (isCheckedFile(file)) { - grid.itemLayout.setBackgroundColor( - ContextCompat.getColor(mContext, R.color.selected_item_background) - ); - grid.checkbox.setImageDrawable( - viewThemeUtils.platform.tintDrawable(mContext, R.drawable.ic_checkbox_marked, ColorRole.PRIMARY) - ); - } else { - grid.itemLayout.setBackgroundColor( - mContext.getResources().getColor(R.color.bg_default) - ); - grid.checkbox.setImageResource(R.drawable.ic_checkbox_blank_outline); - } - - grid.checkbox.setOnClickListener(v -> - localFileListFragmentInterface.onItemCheckboxClicked(file) - ); - } - - // Thumbnail - grid.thumbnail.setTag(file.hashCode()); - setThumbnail(file, grid.thumbnail, mContext, viewThemeUtils); - - grid.itemLayout.setOnClickListener(v -> - localFileListFragmentInterface.onItemClicked(file) - ); - - if (holder instanceof LocalFileListItemViewHolder item) { - if (file.isDirectory()) { - item.fileSize.setVisibility(View.GONE); - item.fileSeparator.setVisibility(View.GONE); - - if (isWithinEncryptedFolder) { - item.checkbox.setVisibility(View.GONE); - } - - } else { - item.fileSize.setVisibility(View.VISIBLE); - item.fileSeparator.setVisibility(View.VISIBLE); - item.fileSize.setText(DisplayUtils.bytesToHumanReadable(file.length())); - } - - item.lastModification.setText( - DisplayUtils.getRelativeTimestamp(mContext, file.lastModified()) - ); - } - - // Filename - grid.fileName.setText(file.getName()); - } - - public static void setThumbnail(File file, - ImageView thumbnailView, - Context context, - ViewThemeUtils viewThemeUtils) { - if (file.isDirectory()) { - thumbnailView.setImageDrawable(MimeTypeUtil.getDefaultFolderIcon(context, viewThemeUtils)); - } else { - thumbnailView.setImageResource(R.drawable.file); - - /* Cancellation needs do be checked and done before changing the drawable in fileIcon, or - * {@link ThumbnailsCacheManager#cancelPotentialThumbnailWork} will NEVER cancel any task. - */ - boolean allowedToCreateNewThumbnail = ThumbnailsCacheManager.cancelPotentialThumbnailWork(file, - thumbnailView); - - - // get Thumbnail if file is image - if (MimeTypeUtil.isImage(file)) { - // Thumbnail in Cache? - Bitmap thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache( - ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.hashCode() - ); - if (thumbnail != null) { - thumbnailView.setImageBitmap(thumbnail); - } else { - - // generate new Thumbnail - if (allowedToCreateNewThumbnail) { - final ThumbnailsCacheManager.ThumbnailGenerationTask task = - new ThumbnailsCacheManager.ThumbnailGenerationTask(thumbnailView); - if (MimeTypeUtil.isVideo(file)) { - thumbnail = ThumbnailsCacheManager.mDefaultVideo; - } else { - thumbnail = ThumbnailsCacheManager.mDefaultImg; - } - final ThumbnailsCacheManager.AsyncThumbnailDrawable asyncDrawable = - new ThumbnailsCacheManager.AsyncThumbnailDrawable( - context.getResources(), - thumbnail, - task - ); - thumbnailView.setImageDrawable(asyncDrawable); - task.execute(new ThumbnailsCacheManager.ThumbnailGenerationTaskObject(file, null)); - Log_OC.v(TAG, "Executing task to generate a new thumbnail"); - - } // else, already being generated, don't restart it - } - } else { - thumbnailView.setImageDrawable(MimeTypeUtil.getFileTypeIcon(null, - file.getName(), - context, - viewThemeUtils)); - } - } - } - - private File getItem(int position) { - return mFiles.get(position); - } - - @NonNull - @Override - public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - switch (viewType) { - case VIEWTYPE_ITEM, VIEWTYPE_IMAGE: - if (gridView) { - View itemView = LayoutInflater.from(mContext).inflate(R.layout.grid_item, parent, false); - return new LocalFileListGridItemViewHolder(itemView); - } else { - View itemView = LayoutInflater.from(mContext).inflate(R.layout.list_item, parent, false); - return new LocalFileListItemViewHolder(itemView); - } - - case VIEWTYPE_FOOTER: - View itemView = LayoutInflater.from(mContext).inflate(R.layout.list_footer, parent, false); - return new LocalFileListFooterViewHolder(itemView); - - case VIEWTYPE_HEADER: - View headerItemView = LayoutInflater.from(mContext).inflate(R.layout.storage_permission_warning_banner, parent, false); - return new StoragePermissionBannerViewHolder(mContext, headerItemView); - default: - throw new IllegalArgumentException("Invalid viewType: " + viewType); - } - } - - /** - * Change the adapted directory for a new one - * - * @param directory New file to adapt. Can be NULL, meaning "no content to adapt". - */ - public void swapDirectory(final File directory) { - localFileListFragmentInterface.setLoading(true); - currentOffset = 0; - - singleThreadExecutor.execute(() -> { - // Load first page of folders - List firstPage = FileHelper.INSTANCE.listDirectoryEntries(directory, currentOffset, PAGE_SIZE, true); - - if (!firstPage.isEmpty()) { - firstPage = sortAndFilterHiddenEntries(firstPage); - } - - currentOffset += PAGE_SIZE; - updateUIForFirstPage(firstPage); - - // Load remaining folders, then all files - loadRemainingEntries(directory, true); - - // Reset for files - currentOffset = 0; - - loadRemainingEntries(directory, false); - }); - } - - @SuppressLint("NotifyDataSetChanged") - private void updateUIForFirstPage(List firstPage) { - new Handler(Looper.getMainLooper()).post(() -> { - mFiles.clear(); - mFilesAll.clear(); - mFiles.addAll(firstPage); - mFilesAll.addAll(firstPage); - notifyDataSetChanged(); - localFileListFragmentInterface.setLoading(false); - }); - } - - private List sortAndFilterHiddenEntries(List nextPage) { - boolean showHiddenFiles = preferences.isShowHiddenFilesEnabled(); - FileSortOrder sortOrder = preferences.getSortOrderByType(FileSortOrder.Type.localFileListView); - - if (!showHiddenFiles) { - nextPage = filterHiddenFiles(nextPage); - } - - return sortOrder.sortLocalFiles(nextPage); - } - - private void loadRemainingEntries(File directory, boolean fetchFolders) { - while (true) { - List nextPage = FileHelper.INSTANCE.listDirectoryEntries(directory, currentOffset, PAGE_SIZE, fetchFolders); - if (nextPage.isEmpty()) { - break; - } - - nextPage = sortAndFilterHiddenEntries(nextPage); - - currentOffset += PAGE_SIZE; - notifyItemRange(nextPage); - } - } - - private void notifyItemRange(List updatedList) { - new Handler(Looper.getMainLooper()).post(() -> { - int headerOffset = shouldShowHeader() ? 1 : 0; - int startPositionInAdapter = mFiles.size() + headerOffset; - int itemCount = updatedList.size(); - - mFiles.addAll(updatedList); - mFilesAll.addAll(updatedList); - - Log_OC.d(TAG, "notifyItemRange, item size: " + mFilesAll.size()); - - notifyItemRangeInserted(startPositionInAdapter, itemCount); - }); - } - - @SuppressLint("NotifyDataSetChanged") - public void setSortOrder(FileSortOrder sortOrder) { - localFileListFragmentInterface.setLoading(true); - singleThreadExecutor.execute(() -> { - List sortedCopy = new ArrayList<>(mFiles); - sortedCopy = sortOrder.sortLocalFiles(sortedCopy); - - final List finalSortedCopy = sortedCopy; - new Handler(Looper.getMainLooper()).post(() -> { - mFiles = finalSortedCopy; - mFilesAll = new ArrayList<>(finalSortedCopy); - notifyDataSetChanged(); - localFileListFragmentInterface.setLoading(false); - }); - }); - } - - @SuppressLint("NotifyDataSetChanged") - public void filter(String text) { - if (text.isEmpty()) { - mFiles = mFilesAll; - } else { - List result = new ArrayList<>(); - String filterText = text.toLowerCase(Locale.getDefault()); - for (File file : mFilesAll) { - if (file.getName().toLowerCase(Locale.getDefault()).contains(filterText)) { - result.add(file); - } - } - mFiles = result; - } - notifyDataSetChanged(); - } - - /** - * Filter for hidden files - * - * @param files ArrayList of files to filter - * @return Non-hidden files - */ - private List filterHiddenFiles(List files) { - List ret = new ArrayList<>(); - - for (File file : files) { - if (!file.isHidden()) { - ret.add(file); - } - } - return ret; - } - - private String getFooterText() { - int filesCount = 0; - int foldersCount = 0; - - for (File file : mFiles) { - if (file.isDirectory()) { - foldersCount++; - } else { - if (!file.isHidden()) { - filesCount++; - } - } - } - - return generateFooterText(filesCount, foldersCount); - } - - private String generateFooterText(int filesCount, int foldersCount) { - String output; - Resources resources = mContext.getResources(); - - if (filesCount + foldersCount <= 0) { - output = ""; - } else if (foldersCount <= 0) { - output = resources.getQuantityString(R.plurals.file_list__footer__file, filesCount, filesCount); - } else if (filesCount <= 0) { - output = resources.getQuantityString(R.plurals.file_list__footer__folder, foldersCount, foldersCount); - } else { - output = resources.getQuantityString(R.plurals.file_list__footer__file, filesCount, filesCount) + ", " + - resources.getQuantityString(R.plurals.file_list__footer__folder, foldersCount, foldersCount); - } - - return output; - } - - public void setGridView(boolean gridView) { - this.gridView = gridView; - } - - public int checkedFilesCount() { - return checkedFiles.size(); - } - - private static class LocalFileListItemViewHolder extends LocalFileListGridItemViewHolder { - private final TextView fileSize; - private final TextView lastModification; - private final TextView fileSeparator; - - private LocalFileListItemViewHolder(View itemView) { - super(itemView); - - fileSize = itemView.findViewById(R.id.file_size); - fileSeparator = itemView.findViewById(R.id.file_separator); - lastModification = itemView.findViewById(R.id.last_mod); - - itemView.findViewById(R.id.sharedAvatars).setVisibility(View.GONE); - itemView.findViewById(R.id.overflow_menu).setVisibility(View.GONE); - itemView.findViewById(R.id.tagsGroup).setVisibility(View.GONE); - } - } - - private static class LocalFileListGridItemViewHolder extends RecyclerView.ViewHolder { - protected final TextView fileName; - protected final ImageView thumbnail; - protected final ImageView checkbox; - protected final LinearLayout itemLayout; - - private LocalFileListGridItemViewHolder(View itemView) { - super(itemView); - - fileName = itemView.findViewById(R.id.Filename); - thumbnail = itemView.findViewById(R.id.thumbnail); - checkbox = itemView.findViewById(R.id.custom_checkbox); - itemLayout = itemView.findViewById(R.id.ListItemLayout); - - itemView.findViewById(R.id.sharedIcon).setVisibility(View.GONE); - itemView.findViewById(R.id.favorite_action).setVisibility(View.GONE); - itemView.findViewById(R.id.localFileIndicator).setVisibility(View.GONE); - } - } - - private static class LocalFileListFooterViewHolder extends RecyclerView.ViewHolder { - private final TextView footerText; - - private LocalFileListFooterViewHolder(View itemView) { - super(itemView); - - footerText = itemView.findViewById(R.id.footerText); - } - } - - @SuppressLint("NotifyDataSetChanged") - @VisibleForTesting - public void setFiles(List newFiles) { - mFiles = newFiles; - mFilesAll = new ArrayList<>(mFiles); - - notifyDataSetChanged(); - localFileListFragmentInterface.setLoading(false); - } - - public void cleanup() { - singleThreadExecutor.shutdown(); - } -} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/localFileList/LocalFileListAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/localFileList/LocalFileListAdapter.kt new file mode 100644 index 000000000000..ef877a2d4f14 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/localFileList/LocalFileListAdapter.kt @@ -0,0 +1,327 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.adapter.localFileList + +import android.annotation.SuppressLint +import android.app.Activity +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.annotation.VisibleForTesting +import androidx.recyclerview.widget.RecyclerView +import com.nextcloud.client.preferences.AppPreferences +import com.nextcloud.utils.FileHelper +import com.owncloud.android.R +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.ui.adapter.FilterableListAdapter +import com.owncloud.android.ui.adapter.storagePermissionBanner.StoragePermissionBannerViewHolder +import com.owncloud.android.ui.interfaces.LocalFileListFragmentInterface +import com.owncloud.android.utils.FileSortOrder +import com.owncloud.android.utils.MimeTypeUtil +import com.owncloud.android.utils.PermissionUtil +import com.owncloud.android.utils.theme.ViewThemeUtils +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.File +import java.util.Locale + +@Suppress("LongParameterList", "TooManyFunctions") +class LocalFileListAdapter( + localFolderPickerMode: Boolean, + private val fragmentInterface: LocalFileListFragmentInterface, + private val preferences: AppPreferences, + private val activity: Activity, + viewThemeUtils: ViewThemeUtils, + private val isWithinEncryptedFolder: Boolean +) : RecyclerView.Adapter(), + FilterableListAdapter { + + private var files: MutableList = mutableListOf() + private var allFiles: MutableList = mutableListOf() + private val checkedFiles: MutableSet = linkedSetOf() + private var currentOffset = 0 + + var gridView: Boolean = false + + private val itemBinder = LocalFileListItemBinder( + activity, + viewThemeUtils, + fragmentInterface, + localFolderPickerMode, + isWithinEncryptedFolder, + ::isCheckedFile + ) + + @OptIn(ExperimentalCoroutinesApi::class) + private val backgroundScope = CoroutineScope(SupervisorJob() + Dispatchers.IO.limitedParallelism(1)) + + init { + setHasStableIds(true) + } + + val filesCount: Int + get() = files.size + + val checkedFilesPath: Array + get() { + val result = FileHelper.listFilesRecursive(checkedFiles) + Log_OC.d(TAG, "Returning ${result.size} selected files") + return result.toTypedArray() + } + + fun isCheckedFile(file: File): Boolean = checkedFiles.contains(file) + + fun addCheckedFile(file: File) { + checkedFiles.add(file) + } + + fun removeCheckedFile(file: File) { + checkedFiles.remove(file) + } + + fun addAllFilesToCheckedFiles() { + if (isWithinEncryptedFolder) { + checkedFiles.addAll(allFiles.filter { it.isFile }) + } else { + checkedFiles.addAll(files) + } + } + + fun removeAllFilesFromCheckedFiles() { + checkedFiles.clear() + } + + fun checkedFilesCount(): Int = checkedFiles.size + + fun getItemPosition(file: File): Int = files.indexOf(file) + + private fun shouldShowHeader(): Boolean = !PermissionUtil.checkStoragePermission(activity) + + private val headerOffset: Int + get() = if (shouldShowHeader()) HEADER_ITEM_COUNT else 0 + + override fun getItemCount(): Int = files.size + FOOTER_ITEM_COUNT + headerOffset + + override fun getItemId(position: Int): Long = getStableItemId(position, headerOffset, files) + + override fun getItemViewType(position: Int): Int { + val offset = headerOffset + + return when { + offset == HEADER_ITEM_COUNT && position == 0 -> VIEW_TYPE_HEADER + position == files.size + offset -> VIEW_TYPE_FOOTER + MimeTypeUtil.isImageOrVideo(files[position - offset]) -> VIEW_TYPE_IMAGE + else -> VIEW_TYPE_ITEM + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val inflater = LayoutInflater.from(activity) + + return when (viewType) { + VIEW_TYPE_ITEM, VIEW_TYPE_IMAGE -> if (gridView) { + LocalFileListGridItemViewHolder(inflater.inflate(R.layout.grid_item, parent, false)) + } else { + LocalFileListItemViewHolder(inflater.inflate(R.layout.list_item, parent, false)) + } + + VIEW_TYPE_FOOTER -> LocalFileListFooterViewHolder(inflater.inflate(R.layout.list_footer, parent, false)) + + VIEW_TYPE_HEADER -> StoragePermissionBannerViewHolder( + activity, + inflater.inflate(R.layout.storage_permission_warning_banner, parent, false) + ) + + else -> throw IllegalArgumentException("Invalid viewType: $viewType") + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val offset = headerOffset + val isHeader = offset == HEADER_ITEM_COUNT && position == 0 + val isFooter = position == files.size + offset + + when { + // the header has no dynamic binding + isHeader -> Unit + + isFooter -> (holder as LocalFileListFooterViewHolder).footerText.text = footerText + + else -> files.getOrNull(position - offset)?.let { file -> + itemBinder.bind(holder as LocalFileListGridItemViewHolder, file) + } + } + } + + /** + * Change the adapted directory for a new one + * + * @param directory New file to adapt. Can be NULL, meaning "no content to adapt". + */ + fun swapDirectory(directory: File?) { + fragmentInterface.setLoading(true) + currentOffset = 0 + + backgroundScope.launch { + showFirstPage(loadFirstPage(directory)) + + // load the remaining folders first, then all files + loadRemainingEntries(directory, fetchFolders = true) + + currentOffset = 0 + loadRemainingEntries(directory, fetchFolders = false) + } + } + + private fun loadFirstPage(directory: File?): List { + val firstPage = FileHelper.listDirectoryEntries(directory, currentOffset, PAGE_SIZE, true) + currentOffset += PAGE_SIZE + + return if (firstPage.isEmpty()) firstPage else sortAndFilterHiddenEntries(firstPage) + } + + private suspend fun loadRemainingEntries(directory: File?, fetchFolders: Boolean) { + while (true) { + val nextPage = FileHelper.listDirectoryEntries(directory, currentOffset, PAGE_SIZE, fetchFolders) + if (nextPage.isEmpty()) { + return + } + + currentOffset += PAGE_SIZE + appendPage(sortAndFilterHiddenEntries(nextPage)) + } + } + + private fun sortAndFilterHiddenEntries(page: List): List { + val visibleEntries = if (preferences.isShowHiddenFilesEnabled) page else page.filterNot { it.isHidden } + val sortOrder = preferences.getSortOrderByType(FileSortOrder.Type.localFileListView) + + return sortOrder.sortLocalFiles(visibleEntries.toMutableList()) + } + + @SuppressLint("NotifyDataSetChanged") + private suspend fun showFirstPage(firstPage: List) = withContext(Dispatchers.Main) { + files = firstPage.toMutableList() + allFiles = firstPage.toMutableList() + + notifyDataSetChanged() + fragmentInterface.setLoading(false) + } + + private suspend fun appendPage(page: List) = withContext(Dispatchers.Main) { + val startPositionInAdapter = files.size + headerOffset + + files.addAll(page) + allFiles.addAll(page) + + Log_OC.d(TAG, "appendPage, item size: ${allFiles.size}") + notifyItemRangeInserted(startPositionInAdapter, page.size) + } + + @SuppressLint("NotifyDataSetChanged") + fun setSortOrder(sortOrder: FileSortOrder) { + fragmentInterface.setLoading(true) + + backgroundScope.launch { + val sortedFiles = sortOrder.sortLocalFiles(files.toMutableList()) + + withContext(Dispatchers.Main) { + files = sortedFiles.toMutableList() + allFiles = sortedFiles.toMutableList() + + notifyDataSetChanged() + fragmentInterface.setLoading(false) + } + } + } + + @SuppressLint("NotifyDataSetChanged") + override fun filter(text: String) { + files = if (text.isEmpty()) allFiles else filterByName(allFiles, text).toMutableList() + notifyDataSetChanged() + } + + private val footerText: String + get() { + val (filesCount, foldersCount) = countFooterEntries(files) + return generateFooterText(filesCount, foldersCount) + } + + private fun generateFooterText(filesCount: Int, foldersCount: Int): String { + val resources = activity.resources + val fileText = resources.getQuantityString(R.plurals.file_list__footer__file, filesCount, filesCount) + val folderText = resources.getQuantityString(R.plurals.file_list__footer__folder, foldersCount, foldersCount) + + return when { + filesCount + foldersCount <= 0 -> "" + foldersCount <= 0 -> fileText + filesCount <= 0 -> folderText + else -> "$fileText, $folderText" + } + } + + @SuppressLint("NotifyDataSetChanged") + @VisibleForTesting + fun setFiles(newFiles: List) { + files = newFiles.toMutableList() + allFiles = newFiles.toMutableList() + + notifyDataSetChanged() + fragmentInterface.setLoading(false) + } + + fun cleanup() { + backgroundScope.cancel() + } + + companion object { + private val TAG: String = LocalFileListAdapter::class.java.simpleName + + private const val VIEW_TYPE_ITEM = 0 + private const val VIEW_TYPE_FOOTER = 1 + private const val VIEW_TYPE_IMAGE = 2 + private const val VIEW_TYPE_HEADER = 3 + + private const val PAGE_SIZE = 50 + private const val HEADER_ITEM_COUNT = 1 + private const val FOOTER_ITEM_COUNT = 1 + + private const val HEADER_ID = Long.MIN_VALUE + private const val FOOTER_ID = Long.MIN_VALUE + 1 + + @VisibleForTesting + fun getStableItemId(position: Int, headerOffset: Int, files: List): Long { + if (headerOffset == HEADER_ITEM_COUNT && position == 0) { + return HEADER_ID + } + + val file = files.getOrNull(position - headerOffset) + return file?.absolutePath?.hashCode()?.toLong() ?: FOOTER_ID + } + + @VisibleForTesting + fun filterByName(files: List, text: String): List { + val filterText = text.lowercase(Locale.getDefault()) + return files.filter { it.name.lowercase(Locale.getDefault()).contains(filterText) } + } + + /** + * Counts the entries shown in the footer: folders are always counted, files only when they are not hidden. + * + * @return amount of files and amount of folders + */ + @VisibleForTesting + fun countFooterEntries(files: List): Pair { + val (folders, plainFiles) = files.partition { it.isDirectory } + return plainFiles.count { !it.isHidden } to folders.size + } + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/localFileList/LocalFileListFooterViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/localFileList/LocalFileListFooterViewHolder.kt new file mode 100644 index 000000000000..8191eb4c18b8 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/localFileList/LocalFileListFooterViewHolder.kt @@ -0,0 +1,16 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.adapter.localFileList + +import android.view.View +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.owncloud.android.R + +internal class LocalFileListFooterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val footerText: TextView = itemView.findViewById(R.id.footerText) +} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/localFileList/LocalFileListGridItemViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/localFileList/LocalFileListGridItemViewHolder.kt new file mode 100644 index 000000000000..4ce3430aff75 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/localFileList/LocalFileListGridItemViewHolder.kt @@ -0,0 +1,27 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.adapter.localFileList + +import android.view.View +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.owncloud.android.R + +internal open class LocalFileListGridItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val fileName: TextView = itemView.findViewById(R.id.Filename) + val thumbnail: ImageView = itemView.findViewById(R.id.thumbnail) + val checkbox: ImageView = itemView.findViewById(R.id.custom_checkbox) + val itemLayout: LinearLayout = itemView.findViewById(R.id.ListItemLayout) + + init { + itemView.findViewById(R.id.sharedIcon).visibility = View.GONE + itemView.findViewById(R.id.favorite_action).visibility = View.GONE + itemView.findViewById(R.id.localFileIndicator).visibility = View.GONE + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/localFileList/LocalFileListItemBinder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/localFileList/LocalFileListItemBinder.kt new file mode 100644 index 000000000000..e52e0790d957 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/localFileList/LocalFileListItemBinder.kt @@ -0,0 +1,84 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.adapter.localFileList + +import android.app.Activity +import android.view.View +import androidx.core.content.ContextCompat +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.owncloud.android.R +import com.owncloud.android.ui.interfaces.LocalFileListFragmentInterface +import com.owncloud.android.utils.DisplayUtils +import com.owncloud.android.utils.theme.ViewThemeUtils +import java.io.File + +@Suppress("LongParameterList") +internal class LocalFileListItemBinder( + private val activity: Activity, + private val viewThemeUtils: ViewThemeUtils, + private val fragmentInterface: LocalFileListFragmentInterface, + private val localFolderPickerMode: Boolean, + private val isWithinEncryptedFolder: Boolean, + private val isCheckedFile: (File) -> Boolean +) { + fun bind(holder: LocalFileListGridItemViewHolder, file: File) { + bindSelection(holder, file) + bindThumbnail(holder, file) + + holder.itemLayout.setOnClickListener { fragmentInterface.onItemClicked(file) } + + if (holder is LocalFileListItemViewHolder) { + bindFileDetails(holder, file) + } + + holder.fileName.text = file.name + } + + private fun bindSelection(holder: LocalFileListGridItemViewHolder, file: File) { + if (localFolderPickerMode) { + holder.itemLayout.setBackgroundColor(ContextCompat.getColor(activity, R.color.bg_default)) + holder.checkbox.visibility = View.GONE + return + } + + holder.checkbox.visibility = View.VISIBLE + + if (isCheckedFile(file)) { + holder.itemLayout.setBackgroundColor(ContextCompat.getColor(activity, R.color.selected_item_background)) + holder.checkbox.setImageDrawable( + viewThemeUtils.platform.tintDrawable(activity, R.drawable.ic_checkbox_marked, ColorRole.PRIMARY) + ) + } else { + holder.itemLayout.setBackgroundColor(ContextCompat.getColor(activity, R.color.bg_default)) + holder.checkbox.setImageResource(R.drawable.ic_checkbox_blank_outline) + } + + holder.checkbox.setOnClickListener { fragmentInterface.onItemCheckboxClicked(file) } + } + + private fun bindThumbnail(holder: LocalFileListGridItemViewHolder, file: File) { + holder.thumbnail.tag = file.hashCode() + LocalFileThumbnailBinder.setThumbnail(file, holder.thumbnail, activity, viewThemeUtils) + } + + private fun bindFileDetails(holder: LocalFileListItemViewHolder, file: File) { + if (file.isDirectory) { + holder.fileSize.visibility = View.GONE + holder.fileSeparator.visibility = View.GONE + + if (isWithinEncryptedFolder) { + holder.checkbox.visibility = View.GONE + } + } else { + holder.fileSize.visibility = View.VISIBLE + holder.fileSeparator.visibility = View.VISIBLE + holder.fileSize.text = DisplayUtils.bytesToHumanReadable(file.length()) + } + + holder.lastModification.text = DisplayUtils.getRelativeTimestamp(activity, file.lastModified()) + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/localFileList/LocalFileListItemViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/localFileList/LocalFileListItemViewHolder.kt new file mode 100644 index 000000000000..e94d9c709dbc --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/localFileList/LocalFileListItemViewHolder.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.adapter.localFileList + +import android.view.View +import android.widget.TextView +import com.owncloud.android.R + +internal class LocalFileListItemViewHolder(itemView: View) : LocalFileListGridItemViewHolder(itemView) { + val fileSize: TextView = itemView.findViewById(R.id.file_size) + val fileSeparator: TextView = itemView.findViewById(R.id.file_separator) + val lastModification: TextView = itemView.findViewById(R.id.last_mod) + + init { + itemView.findViewById(R.id.sharedAvatars).visibility = View.GONE + itemView.findViewById(R.id.overflow_menu).visibility = View.GONE + itemView.findViewById(R.id.tagsGroup).visibility = View.GONE + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/localFileList/LocalFileThumbnailBinder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/localFileList/LocalFileThumbnailBinder.kt new file mode 100644 index 000000000000..20d5e564c4df --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/localFileList/LocalFileThumbnailBinder.kt @@ -0,0 +1,74 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.adapter.localFileList + +import android.content.Context +import android.widget.ImageView +import com.owncloud.android.R +import com.owncloud.android.datamodel.ThumbnailsCacheManager +import com.owncloud.android.datamodel.ThumbnailsCacheManager.AsyncThumbnailDrawable +import com.owncloud.android.datamodel.ThumbnailsCacheManager.ThumbnailGenerationTask +import com.owncloud.android.datamodel.ThumbnailsCacheManager.ThumbnailGenerationTaskObject +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.utils.MimeTypeUtil +import com.owncloud.android.utils.theme.ViewThemeUtils +import java.io.File + +object LocalFileThumbnailBinder { + private val TAG: String = LocalFileThumbnailBinder::class.java.simpleName + + fun setThumbnail(file: File, thumbnailView: ImageView, context: Context, viewThemeUtils: ViewThemeUtils) { + if (file.isDirectory) { + thumbnailView.setImageDrawable(MimeTypeUtil.getDefaultFolderIcon(context, viewThemeUtils)) + return + } + + thumbnailView.setImageResource(R.drawable.file) + + // Cancellation needs to be checked and done before changing the drawable in fileIcon, or + // ThumbnailsCacheManager#cancelPotentialThumbnailWork will NEVER cancel any task. + val allowedToCreateNewThumbnail = ThumbnailsCacheManager.cancelPotentialThumbnailWork(file, thumbnailView) + + if (!MimeTypeUtil.isImage(file)) { + thumbnailView.setImageDrawable(MimeTypeUtil.getFileTypeIcon(null, file.name, context, viewThemeUtils)) + return + } + + setImageThumbnail(file, thumbnailView, context, allowedToCreateNewThumbnail) + } + + private fun setImageThumbnail( + file: File, + thumbnailView: ImageView, + context: Context, + allowedToCreateNewThumbnail: Boolean + ) { + val cachedThumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache( + ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.hashCode() + ) + + when { + cachedThumbnail != null -> thumbnailView.setImageBitmap(cachedThumbnail) + + // when a thumbnail is already being generated, don't restart it + allowedToCreateNewThumbnail -> generateThumbnail(file, thumbnailView, context) + } + } + + private fun generateThumbnail(file: File, thumbnailView: ImageView, context: Context) { + val task = ThumbnailGenerationTask(thumbnailView) + val placeholder = if (MimeTypeUtil.isVideo(file)) { + ThumbnailsCacheManager.mDefaultVideo + } else { + ThumbnailsCacheManager.mDefaultImg + } + + thumbnailView.setImageDrawable(AsyncThumbnailDrawable(context.resources, placeholder, task)) + task.execute(ThumbnailGenerationTaskObject(file, null)) + Log_OC.v(TAG, "Executing task to generate a new thumbnail") + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/ConflictsResolveDialog.kt b/app/src/main/java/com/owncloud/android/ui/dialog/ConflictsResolveDialog.kt index db515bb7aa30..055213311ea8 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/ConflictsResolveDialog.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/ConflictsResolveDialog.kt @@ -28,7 +28,7 @@ import com.owncloud.android.datamodel.OCFile import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.datamodel.ThumbnailsCacheManager.ThumbnailGenerationTask import com.owncloud.android.lib.common.utils.Log_OC -import com.owncloud.android.ui.adapter.LocalFileListAdapter +import com.owncloud.android.ui.adapter.localFileList.LocalFileThumbnailBinder import com.owncloud.android.ui.dialog.parcel.ConflictDialogData import com.owncloud.android.ui.dialog.parcel.ConflictFileData import com.owncloud.android.utils.DisplayUtils @@ -215,13 +215,16 @@ class ConflictsResolveDialog : } private fun setThumbnailsForFileConflicts() { - binding.leftThumbnail.tag = leftDataFile.hashCode() + val localFile = leftDataFile ?: return + val currentContext = context ?: return + + binding.leftThumbnail.tag = localFile.hashCode() binding.rightThumbnail.tag = rightDataFile.hashCode() - LocalFileListAdapter.setThumbnail( - leftDataFile, + LocalFileThumbnailBinder.setThumbnail( + localFile, binding.leftThumbnail, - context, + currentContext, viewThemeUtils ) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/ExtendedListFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/ExtendedListFragment.kt index eb4ae2adfffd..aed275f33c5c 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/ExtendedListFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/ExtendedListFragment.kt @@ -68,7 +68,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.LocalFileListAdapter +import com.owncloud.android.ui.adapter.localFileList.LocalFileListAdapter import com.owncloud.android.ui.adapter.OCFileListAdapter import com.owncloud.android.ui.events.SearchEvent import com.owncloud.android.utils.theme.ViewThemeUtils diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/LocalFileListFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/LocalFileListFragment.java index 14d5cbc9cf6e..6c433b29db20 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/LocalFileListFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/LocalFileListFragment.java @@ -23,7 +23,7 @@ import com.nextcloud.client.di.Injectable; import com.owncloud.android.R; import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.ui.adapter.LocalFileListAdapter; +import com.owncloud.android.ui.adapter.localFileList.LocalFileListAdapter; import com.owncloud.android.ui.interfaces.LocalFileListFragmentInterface; import com.owncloud.android.utils.DisplayUtils; import com.owncloud.android.utils.FileSortOrder; diff --git a/app/src/main/java/com/owncloud/android/ui/interfaces/LocalFileListFragmentInterface.kt b/app/src/main/java/com/owncloud/android/ui/interfaces/LocalFileListFragmentInterface.kt index a804f9d90911..fca93caaacc3 100644 --- a/app/src/main/java/com/owncloud/android/ui/interfaces/LocalFileListFragmentInterface.kt +++ b/app/src/main/java/com/owncloud/android/ui/interfaces/LocalFileListFragmentInterface.kt @@ -1,23 +1,16 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2017 Mario Danic - * SPDX-FileCopyrightText: 2017 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.owncloud.android.ui.interfaces; +package com.owncloud.android.ui.interfaces -import com.owncloud.android.ui.adapter.LocalFileListAdapter; +import java.io.File -import java.io.File; - -/** - * Interface for communication between {@link com.owncloud.android.ui.fragment.LocalFileListFragment} - * and {@link LocalFileListAdapter} - */ -public interface LocalFileListFragmentInterface { - int getColumnsCount(); - void onItemClicked(File file); - void onItemCheckboxClicked(File file); - void setLoading(boolean loading); +interface LocalFileListFragmentInterface { + val columnsCount: Int + fun onItemClicked(file: File?) + fun onItemCheckboxClicked(file: File?) + fun setLoading(loading: Boolean) } diff --git a/app/src/test/java/com/owncloud/android/ui/adapter/LocalFileListAdapterTest.kt b/app/src/test/java/com/owncloud/android/ui/adapter/LocalFileListAdapterTest.kt deleted file mode 100644 index 6518ca28505a..000000000000 --- a/app/src/test/java/com/owncloud/android/ui/adapter/LocalFileListAdapterTest.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2026 Alper Ozturk - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -package com.owncloud.android.ui.adapter - -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Test -import java.io.File - -class LocalFileListAdapterTest { - - private fun sampleFiles(): List = listOf(File("/sdcard/a.jpg"), File("/sdcard/b.png"), File("/sdcard/c.mp4")) - - private fun assertAllPositionsHaveUniqueIds(headerOffset: Int, files: List) { - val itemCount = files.size + 1 + headerOffset - - val ids = mutableSetOf() - for (position in 0 until itemCount) { - val id = LocalFileListAdapter.getStableItemId(position, headerOffset, files) - assertTrue("Duplicate stable ID $id at position $position", ids.add(id)) - } - assertEquals("Every position must produce a distinct stable ID", itemCount, ids.size) - } - - @Test - fun stableIdsAreUniqueWhenHeaderIsVisible() { - assertAllPositionsHaveUniqueIds(headerOffset = 1, files = sampleFiles()) - } - - @Test - fun stableIdsAreUniqueWhenHeaderIsHidden() { - assertAllPositionsHaveUniqueIds(headerOffset = 0, files = sampleFiles()) - } - - @Test - fun headerAndFooterKeepDedicatedIds() { - val files = sampleFiles() - - val headerId = LocalFileListAdapter.getStableItemId(0, 1, files) - val footerId = LocalFileListAdapter.getStableItemId(files.size + 1, 1, files) - - assertEquals(Long.MIN_VALUE, headerId) - assertEquals(Long.MIN_VALUE + 1, footerId) - } -} diff --git a/app/src/test/java/com/owncloud/android/ui/adapter/localFileList/LocalFileListAdapterTest.kt b/app/src/test/java/com/owncloud/android/ui/adapter/localFileList/LocalFileListAdapterTest.kt new file mode 100644 index 000000000000..73e55368c659 --- /dev/null +++ b/app/src/test/java/com/owncloud/android/ui/adapter/localFileList/LocalFileListAdapterTest.kt @@ -0,0 +1,110 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.adapter.localFileList + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File + +class LocalFileListAdapterTest { + + @get:Rule + val temporaryFolder = TemporaryFolder() + + private fun sampleFiles(): List = listOf(File("/sdcard/a.jpg"), File("/sdcard/b.png"), File("/sdcard/c.mp4")) + + private fun assertAllPositionsHaveUniqueIds(headerOffset: Int, files: List) { + val itemCount = files.size + 1 + headerOffset + + val ids = mutableSetOf() + for (position in 0 until itemCount) { + val id = LocalFileListAdapter.getStableItemId(position, headerOffset, files) + assertTrue("Duplicate stable ID $id at position $position", ids.add(id)) + } + assertEquals("Every position must produce a distinct stable ID", itemCount, ids.size) + } + + @Test + fun stableIdsAreUniqueWhenHeaderIsVisible() { + assertAllPositionsHaveUniqueIds(headerOffset = 1, files = sampleFiles()) + } + + @Test + fun stableIdsAreUniqueWhenHeaderIsHidden() { + assertAllPositionsHaveUniqueIds(headerOffset = 0, files = sampleFiles()) + } + + @Test + fun headerAndFooterKeepDedicatedIds() { + val files = sampleFiles() + + val headerId = LocalFileListAdapter.getStableItemId(0, 1, files) + val footerId = LocalFileListAdapter.getStableItemId(files.size + 1, 1, files) + + assertEquals(Long.MIN_VALUE, headerId) + assertEquals(Long.MIN_VALUE + 1, footerId) + } + + @Test + fun stableIdIsDerivedFromAbsolutePath() { + val file = File("/sdcard/a.jpg") + + val id = LocalFileListAdapter.getStableItemId(0, 0, listOf(file)) + + assertEquals(file.absolutePath.hashCode().toLong(), id) + } + + @Test + fun filterMatchesFileNamesIgnoringCase() { + val files = listOf(File("/sdcard/Holiday.jpg"), File("/sdcard/invoice.pdf"), File("/sdcard/HOLIDAY-2.png")) + + val result = LocalFileListAdapter.filterByName(files, "holiday") + + assertEquals(listOf(files[0], files[2]), result) + } + + @Test + fun filterKeepsOriginalOrderAndDropsNonMatchingFiles() { + val files = sampleFiles() + + assertEquals(emptyList(), LocalFileListAdapter.filterByName(files, "zip")) + assertEquals(files, LocalFileListAdapter.filterByName(files, "")) + } + + @Test + fun filterIgnoresParentDirectoriesOfTheFilePath() { + val files = listOf(File("/sdcard/holiday/invoice.pdf")) + + assertEquals(emptyList(), LocalFileListAdapter.filterByName(files, "holiday")) + } + + @Test + fun footerCountsFoldersEvenWhenHiddenButSkipsHiddenFiles() { + val folder = temporaryFolder.newFolder("folder") + val hiddenFolder = temporaryFolder.newFolder(".hiddenFolder") + val file = temporaryFolder.newFile("invoice.pdf") + val hiddenFile = temporaryFolder.newFile(".hiddenInvoice.pdf") + + val (filesCount, foldersCount) = LocalFileListAdapter.countFooterEntries( + listOf(folder, hiddenFolder, file, hiddenFile) + ) + + assertEquals(1, filesCount) + assertEquals(2, foldersCount) + } + + @Test + fun footerCountsAreZeroForEmptyList() { + val (filesCount, foldersCount) = LocalFileListAdapter.countFooterEntries(emptyList()) + + assertEquals(0, filesCount) + assertEquals(0, foldersCount) + } +}