diff --git a/app/src/main/java/com/nextcloud/client/di/AppModule.java b/app/src/main/java/com/nextcloud/client/di/AppModule.java index 32a0150aa0f3..58dee51ee036 100644 --- a/app/src/main/java/com/nextcloud/client/di/AppModule.java +++ b/app/src/main/java/com/nextcloud/client/di/AppModule.java @@ -57,6 +57,7 @@ import com.owncloud.android.ui.activities.data.files.FilesServiceApiImpl; import com.owncloud.android.ui.activities.data.files.RemoteFilesRepository; import com.owncloud.android.ui.dialog.setupEncryption.CertificateValidator; +import com.owncloud.android.utils.overlay.OverlayManager; import com.owncloud.android.utils.theme.ViewThemeUtils; import org.greenrobot.eventbus.EventBus; @@ -268,4 +269,15 @@ UsersAndGroupsSearchConfig userAndGroupSearchConfig() { CertificateValidator certificateValidator() { return new CertificateValidator(); } + + @Provides + @Singleton + OverlayManager overlayManager( + SyncedFolderProvider syncedFolderProvider, + AppPreferences appPreferences, + ViewThemeUtils viewThemeUtils, + Context context, + UserAccountManager accountManager) { + return new OverlayManager(syncedFolderProvider, appPreferences, viewThemeUtils, context, accountManager); + } } diff --git a/app/src/main/java/com/nextcloud/ui/fileactions/FileActionsBottomSheet.kt b/app/src/main/java/com/nextcloud/ui/fileactions/FileActionsBottomSheet.kt index 28619ed1741f..6ebd22edf63e 100644 --- a/app/src/main/java/com/nextcloud/ui/fileactions/FileActionsBottomSheet.kt +++ b/app/src/main/java/com/nextcloud/ui/fileactions/FileActionsBottomSheet.kt @@ -46,6 +46,7 @@ import com.owncloud.android.ui.activity.ComponentsGetter import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.DisplayUtils.AvatarGenerationListener import com.owncloud.android.utils.FileStorageUtils +import com.owncloud.android.utils.overlay.OverlayManager import com.owncloud.android.utils.theme.ViewThemeUtils import javax.inject.Inject @@ -68,6 +69,9 @@ class FileActionsBottomSheet : @Inject lateinit var syncedFolderProvider: SyncedFolderProvider + @Inject + lateinit var overlayManager: OverlayManager + private lateinit var viewModel: FileActionsViewModel private var _binding: FileActionsBottomSheetBinding? = null @@ -153,7 +157,7 @@ class FileActionsBottomSheet : binding.thumbnailLayout.thumbnailShimmer, syncedFolderProvider.preferences, viewThemeUtils, - syncedFolderProvider + overlayManager ) } } diff --git a/app/src/main/java/com/nextcloud/ui/trashbinFileActions/TrashbinFileActionsBottomSheet.kt b/app/src/main/java/com/nextcloud/ui/trashbinFileActions/TrashbinFileActionsBottomSheet.kt index 82422d9e64ee..89573ec5f443 100644 --- a/app/src/main/java/com/nextcloud/ui/trashbinFileActions/TrashbinFileActionsBottomSheet.kt +++ b/app/src/main/java/com/nextcloud/ui/trashbinFileActions/TrashbinFileActionsBottomSheet.kt @@ -35,6 +35,7 @@ import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.datamodel.ThumbnailsCacheManager import com.owncloud.android.lib.resources.trashbin.model.TrashbinFile import com.owncloud.android.utils.DisplayUtils +import com.owncloud.android.utils.overlay.OverlayManager import com.owncloud.android.utils.theme.ViewThemeUtils import javax.inject.Inject @@ -57,6 +58,9 @@ class TrashbinFileActionsBottomSheet : @Inject lateinit var syncedFolderProvider: SyncedFolderProvider + @Inject + lateinit var overlayManager: OverlayManager + private lateinit var viewModel: TrashbinFileActionsViewModel private var _binding: FileActionsBottomSheetBinding? = null @@ -129,7 +133,7 @@ class TrashbinFileActionsBottomSheet : binding.thumbnailLayout.thumbnailShimmer, syncedFolderProvider.preferences, viewThemeUtils, - syncedFolderProvider + overlayManager ) } } diff --git a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java index 497776f6babf..c999706f9fe3 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java @@ -147,6 +147,24 @@ OCFile getFileByDecryptedRemotePath(String path) { return getFileByPath(ProviderTableMeta.FILE_PATH_DECRYPTED, path); } + /** + * Returns the {@link OCFile} for the given remote path. + * Tries the path as-is first; if not found, appends a trailing "/" for folders. + * + * @param path The file or folder path. + * @return The matching {@link OCFile}, or null if not found. + */ + @Nullable + public OCFile getFileByRemotePath(String path) { + OCFile file = getFileByDecryptedRemotePath(path); + + if (file == null) { + file = getFileByDecryptedRemotePath(path + OCFile.PATH_SEPARATOR); + } + + return file; + } + public void addCreateFileOfflineOperation(String[] localPaths, String[] remotePaths) { if (localPaths.length != remotePaths.length) { Log_OC.d(TAG, "Local path and remote path size do not match"); diff --git a/app/src/main/java/com/owncloud/android/datamodel/SyncedFolderProvider.java b/app/src/main/java/com/owncloud/android/datamodel/SyncedFolderProvider.java index ff4c00cfb269..a2fb5cb0afb4 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/SyncedFolderProvider.java +++ b/app/src/main/java/com/owncloud/android/datamodel/SyncedFolderProvider.java @@ -432,6 +432,10 @@ private ContentValues createContentValuesFromSyncedFolder(SyncedFolder syncedFol * @return true if exist, false otherwise */ public boolean findByRemotePathAndAccount(String remotePath, User user) { + if (user == null) { + return false; + } + boolean result = false; //if path ends with / then remove the last / to work the query right way 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 c93624ba352c..63b5d6951c13 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 @@ -67,6 +67,7 @@ import com.owncloud.android.utils.FileSortOrder; import com.owncloud.android.utils.FileStorageUtils; import com.owncloud.android.utils.MimeTypeUtil; +import com.owncloud.android.utils.overlay.OverlayManager; import com.owncloud.android.utils.theme.CapabilityUtils; import com.owncloud.android.utils.theme.ViewThemeUtils; @@ -134,6 +135,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter recommendedFiles = new ArrayList<>(); private RecommendedFilesAdapter recommendedFilesAdapter; private final OCFileListAdapterHelper helper = new OCFileListAdapterHelper(); + private final OverlayManager overlayManager; public OCFileListAdapter( Activity activity, @@ -144,7 +146,9 @@ public OCFileListAdapter( OCFileListFragmentInterface ocFileListFragmentInterface, boolean argHideItemOptions, boolean gridView, - final ViewThemeUtils viewThemeUtils) { + final ViewThemeUtils viewThemeUtils, + OverlayManager overlayManager) { + this.overlayManager = overlayManager; this.ocFileListFragmentInterface = ocFileListFragmentInterface; this.activity = activity; this.preferences = preferences; @@ -476,7 +480,7 @@ public void bindRecommendedFilesHolder(OCFileListRecommendedItemViewHolder holde } private void bindHolder(@NonNull RecyclerView.ViewHolder holder, ListViewHolder viewHolder, OCFile file) { - ocFileListDelegate.bindViewHolder(viewHolder, file, currentDirectory, searchType); + ocFileListDelegate.bindViewHolder(viewHolder, file, currentDirectory, searchType, overlayManager); if (holder instanceof ListItemViewHolder itemViewHolder) { bindListItemViewHolder(itemViewHolder, 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 index 343659e8938a..9e2d812282cb 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt @@ -37,6 +37,7 @@ import com.owncloud.android.ui.fragment.SearchType import com.owncloud.android.ui.interfaces.OCFileListFragmentInterface import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.EncryptionUtils +import com.owncloud.android.utils.overlay.OverlayManager import com.owncloud.android.utils.theme.ViewThemeUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -175,7 +176,12 @@ class OCFileListDelegate( } } - fun setThumbnail(thumbnail: ImageView, shimmerThumbnail: LoaderImageView?, file: OCFile) { + fun setThumbnail( + thumbnail: ImageView, + shimmerThumbnail: LoaderImageView?, + file: OCFile, + overlayManager: OverlayManager + ) { DisplayUtils.setThumbnail( file, thumbnail, @@ -187,16 +193,22 @@ class OCFileListDelegate( shimmerThumbnail, preferences, viewThemeUtils, - syncFolderProvider + overlayManager ) } @Suppress("MagicNumber") - fun bindViewHolder(viewHolder: ListViewHolder, file: OCFile, currentDirectory: OCFile?, searchType: SearchType?) { + fun bindViewHolder( + viewHolder: ListViewHolder, + file: OCFile, + currentDirectory: OCFile?, + searchType: SearchType?, + overlayManager: OverlayManager + ) { // thumbnail viewHolder.imageFileName?.text = file.fileName viewHolder.thumbnail.tag = file.fileId - setThumbnail(viewHolder.thumbnail, viewHolder.shimmerThumbnail, file) + setThumbnail(viewHolder.thumbnail, viewHolder.shimmerThumbnail, file, overlayManager) // item layout + click listeners bindGridItemLayout(file, viewHolder) diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchCurrentDirItemViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchCurrentDirItemViewHolder.kt index a355cd5d4d0b..d34f33c80b08 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchCurrentDirItemViewHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchCurrentDirItemViewHolder.kt @@ -20,6 +20,7 @@ import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.ui.interfaces.UnifiedSearchCurrentDirItemAction import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.FileStorageUtils +import com.owncloud.android.utils.overlay.OverlayManager import com.owncloud.android.utils.theme.ViewThemeUtils @Suppress("LongParameterList") @@ -31,8 +32,8 @@ class UnifiedSearchCurrentDirItemViewHolder( private val isRTL: Boolean, private val user: User, private val appPreferences: AppPreferences, - private val syncedFolderProvider: SyncedFolderProvider, - private val action: UnifiedSearchCurrentDirItemAction + private val action: UnifiedSearchCurrentDirItemAction, + private val overlayManager: OverlayManager ) : SectionedViewHolder(binding.unifiedSearchCurrentDirItemLayout) { fun bind(file: OCFile) { @@ -49,7 +50,6 @@ class UnifiedSearchCurrentDirItemViewHolder( binding.filename.text = filename } - viewThemeUtils.platform.colorImageView(binding.thumbnail, ColorRole.PRIMARY) DisplayUtils.setThumbnail( file, binding.thumbnail, @@ -61,7 +61,7 @@ class UnifiedSearchCurrentDirItemViewHolder( binding.thumbnailShimmer, appPreferences, viewThemeUtils, - syncedFolderProvider + overlayManager ) binding.more.setOnClickListener { diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchItemViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchItemViewHolder.kt index fcc8f4661f39..b83e9de200b6 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchItemViewHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchItemViewHolder.kt @@ -17,10 +17,16 @@ import com.nextcloud.utils.CalendarEventManager import com.nextcloud.utils.ContactManager import com.nextcloud.utils.GlideHelper import com.nextcloud.utils.extensions.getType +import com.nextcloud.utils.extensions.setVisibleIf +import com.owncloud.android.R import com.owncloud.android.databinding.UnifiedSearchItemBinding import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.lib.common.SearchResultEntry +import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.ui.interfaces.UnifiedSearchListInterface +import com.owncloud.android.utils.DisplayUtils +import com.owncloud.android.utils.MimeTypeUtil +import com.owncloud.android.utils.overlay.OverlayManager import com.owncloud.android.utils.theme.ViewThemeUtils @Suppress("LongParameterList") @@ -31,12 +37,13 @@ class UnifiedSearchItemViewHolder( private val listInterface: UnifiedSearchListInterface, private val filesAction: FilesAction, val context: Context, - private val nextcloudClient: NextcloudClient, - private val viewThemeUtils: ViewThemeUtils + private val viewThemeUtils: ViewThemeUtils, + private val overlayManager: OverlayManager ) : SectionedViewHolder(binding.root) { interface FilesAction { fun showFilesAction(searchResultEntry: SearchResultEntry) + fun loadFileThumbnail(searchResultEntry: SearchResultEntry, onClientReady: (NextcloudClient) -> Unit) } private val contactManager = ContactManager(context) @@ -44,25 +51,77 @@ class UnifiedSearchItemViewHolder( fun bind(entry: SearchResultEntry) { binding.title.text = entry.title - binding.subline.text = entry.subline + bindSubline(entry) + bindLocalFileIndicator(entry) - if (entry.isFile && storageManager.getFileByDecryptedRemotePath(entry.remotePath()) != null) { - binding.localFileIndicator.visibility = View.VISIBLE + val entryType = entry.getType() + bindThumbnail(entry, entryType) + bindMoreButton(entry) + binding.unifiedSearchItemLayout.setOnClickListener { + searchEntryOnClick(entry, entryType) + } + } + + private fun bindSubline(entry: SearchResultEntry) { + if (entry.subline.isNotBlank()) { + binding.subline.visibility = View.VISIBLE + binding.subline.text = entry.subline } else { - binding.localFileIndicator.visibility = View.GONE + binding.subline.visibility = View.GONE + + val paddingInDp = context.resources.getDimension(R.dimen.standard_padding) + val paddingInPx = DisplayUtils.convertDpToPixel(paddingInDp, context) + binding.titleContainer.setPadding(0, paddingInPx, 0, 0) } + } - val entryType = entry.getType() - viewThemeUtils.platform.colorImageView(binding.thumbnail, ColorRole.PRIMARY) - GlideHelper.loadIntoImageView( - context, - nextcloudClient, - entry.thumbnailUrl, - binding.thumbnail, - entryType.iconId(), - circleCrop = entry.rounded - ) + private fun bindLocalFileIndicator(entry: SearchResultEntry) { + val showLocalFileIndicator = + (entry.isFile && storageManager.getFileByDecryptedRemotePath(entry.remotePath()) != null) + binding.localFileIndicator.setVisibleIf(showLocalFileIndicator) + } + + private fun bindThumbnail(entry: SearchResultEntry, entryType: SearchResultEntryType) { + val file = storageManager.getFileByRemotePath(entry.remotePath()) + + if (file?.isFolder == true) { + Log_OC.d("DEBUG FOLDER", "Path: ${entry.remotePath()}, isFolder=${file.isFolder}, " + + "mime=${file?.mimeType}") + viewThemeUtils.platform.colorImageView(binding.thumbnail, ColorRole.PRIMARY) + overlayManager.setFolderOverlayIcon(file, binding.thumbnailOverlayIcon) + } else { + binding.thumbnail.clearColorFilter() + + if (file != null) { + Log_OC.d("DEBUG FILE", "Path: ${entry.remotePath()}, isFolder=${file.isFolder}, " + + "mime=${file?.mimeType}") + + val icon = MimeTypeUtil.getFileTypeIcon( + file.mimeType, + file.fileName, + context, + viewThemeUtils + ) + binding.thumbnail.setImageDrawable(icon) + } else if (entry.thumbnailUrl.isNotBlank()) { + Log_OC.d("DEBUG GLIDE", "URL: " + entry.thumbnailUrl) + + filesAction.loadFileThumbnail(entry) { client -> + GlideHelper.loadIntoImageView( + context, + client, + entry.thumbnailUrl, + binding.thumbnail, + entryType.iconId(), + circleCrop = entry.rounded + ) + } + } // FIXME: Settings, Apps category + } + } + + private fun bindMoreButton(entry: SearchResultEntry) { if (entry.isFile) { binding.more.visibility = View.VISIBLE binding.more.setOnClickListener { @@ -71,10 +130,6 @@ class UnifiedSearchItemViewHolder( } else { binding.more.visibility = View.GONE } - - binding.unifiedSearchItemLayout.setOnClickListener { - searchEntryOnClick(entry, entryType) - } } private fun searchEntryOnClick(entry: SearchResultEntry, entryType: SearchResultEntryType) { diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchListAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchListAdapter.kt index da62218ff348..9b97dc82a219 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchListAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/UnifiedSearchListAdapter.kt @@ -17,7 +17,6 @@ import com.afollestad.sectionedrecyclerview.SectionedRecyclerViewAdapter import com.afollestad.sectionedrecyclerview.SectionedViewHolder import com.nextcloud.client.account.User import com.nextcloud.client.preferences.AppPreferences -import com.nextcloud.common.NextcloudClient import com.owncloud.android.R import com.owncloud.android.databinding.UnifiedSearchCurrentDirectoryItemBinding import com.owncloud.android.databinding.UnifiedSearchEmptyBinding @@ -32,12 +31,13 @@ import com.owncloud.android.ui.interfaces.UnifiedSearchCurrentDirItemAction import com.owncloud.android.ui.interfaces.UnifiedSearchListInterface import com.owncloud.android.ui.unifiedsearch.UnifiedSearchSection import com.owncloud.android.utils.DisplayUtils +import com.owncloud.android.utils.overlay.OverlayManager import com.owncloud.android.utils.theme.ViewThemeUtils /** * This Adapter populates a SectionedRecyclerView with search results by unified search */ -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LongMethod") class UnifiedSearchListAdapter( private val supportsOpeningCalendarContactsLocally: Boolean, private val storageManager: FileDataStorageManager, @@ -48,8 +48,8 @@ class UnifiedSearchListAdapter( private val viewThemeUtils: ViewThemeUtils, private val appPreferences: AppPreferences, private val syncedFolderProvider: SyncedFolderProvider, - private val nextcloudClient: NextcloudClient, - private val currentDirItemAction: UnifiedSearchCurrentDirItemAction + private val currentDirItemAction: UnifiedSearchCurrentDirItemAction, + private val overlayManager: OverlayManager ) : SectionedRecyclerViewAdapter() { companion object { private const val VIEW_TYPE_EMPTY = Int.MAX_VALUE @@ -93,8 +93,8 @@ class UnifiedSearchListAdapter( listInterface, filesAction, context, - nextcloudClient, - viewThemeUtils + viewThemeUtils, + overlayManager ) } @@ -109,8 +109,8 @@ class UnifiedSearchListAdapter( isRTL, user, appPreferences, - syncedFolderProvider, - currentDirItemAction + currentDirItemAction, + overlayManager ) } 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 522626227391..db515bb7aa30 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 @@ -33,6 +33,7 @@ import com.owncloud.android.ui.dialog.parcel.ConflictDialogData import com.owncloud.android.ui.dialog.parcel.ConflictFileData import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.MimeTypeUtil +import com.owncloud.android.utils.overlay.OverlayManager import com.owncloud.android.utils.theme.ViewThemeUtils import java.io.File import javax.inject.Inject @@ -63,6 +64,9 @@ class ConflictsResolveDialog : @Inject lateinit var fileDataStorageManager: FileDataStorageManager + @Inject + lateinit var overlayManager: OverlayManager + enum class Decision { CANCEL, KEEP_BOTH, @@ -232,7 +236,7 @@ class ConflictsResolveDialog : null, syncedFolderProvider.preferences, viewThemeUtils, - syncedFolderProvider + overlayManager ) } 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 828aca199a55..e19b355e283e 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 @@ -114,6 +114,7 @@ import com.owncloud.android.utils.FileSortOrder; import com.owncloud.android.utils.FileStorageUtils; import com.owncloud.android.utils.PermissionUtil; +import com.owncloud.android.utils.overlay.OverlayManager; import com.owncloud.android.utils.theme.ThemeUtils; import org.apache.commons.httpclient.HttpStatus; @@ -206,6 +207,7 @@ public class OCFileListFragment extends ExtendedListFragment implements @Inject ShortcutUtil shortcutUtil; @Inject SyncedFolderProvider syncedFolderProvider; @Inject AppScanOptionalFeature appScanOptionalFeature; + @Inject OverlayManager overlayManager; protected FileFragment.ContainerActivity mContainerActivity; @@ -461,7 +463,8 @@ protected void setAdapter(Bundle args) { this, hideItemOptions, isGridViewPreferred(mFile), - viewThemeUtils + viewThemeUtils, + overlayManager ); setRecyclerViewAdapter(mAdapter); diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/UnifiedSearchFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/UnifiedSearchFragment.kt index 7c8bd588807f..4f94423d584b 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/UnifiedSearchFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/UnifiedSearchFragment.kt @@ -36,6 +36,7 @@ import com.nextcloud.client.di.Injectable import com.nextcloud.client.di.ViewModelFactory import com.nextcloud.client.network.ClientFactory import com.nextcloud.client.preferences.AppPreferences +import com.nextcloud.common.NextcloudClient import com.nextcloud.utils.extensions.getTypedActivity import com.nextcloud.utils.extensions.searchFilesByName import com.nextcloud.utils.extensions.setVisibleIf @@ -62,6 +63,7 @@ import com.owncloud.android.ui.unifiedsearch.UnifiedSearchViewModel import com.owncloud.android.ui.unifiedsearch.filterOutHiddenFiles import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.PermissionUtil +import com.owncloud.android.utils.overlay.OverlayManager import com.owncloud.android.utils.theme.ViewThemeUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -106,6 +108,9 @@ class UnifiedSearchFragment : } } + @Inject + lateinit var overlayManager: OverlayManager + @Inject lateinit var vmFactory: ViewModelFactory @@ -133,6 +138,8 @@ class UnifiedSearchFragment : @Inject lateinit var clock: Clock + @Volatile private var client: NextcloudClient? = null + private var listOfHiddenFiles = ArrayList() private var showMoreActions = false private var currentDir: OCFile? = null @@ -371,37 +378,30 @@ class UnifiedSearchFragment : val syncedFolderProvider = SyncedFolderProvider(requireContext().contentResolver, appPreferences, clock) val gridLayoutManager = GridLayoutManager(requireContext(), 1) - lifecycleScope.launch(Dispatchers.IO) { - val client = - getTypedActivity(FileActivity::class.java)?.clientRepository?.getNextcloudClient() ?: return@launch + adapter = UnifiedSearchListAdapter( + supportsOpeningCalendarContactsLocally(), + storageManager, + this@UnifiedSearchFragment, + this@UnifiedSearchFragment, + currentAccountProvider.user, + requireContext(), + viewThemeUtils, + appPreferences, + syncedFolderProvider, + this@UnifiedSearchFragment, + overlayManager + ) + + adapter.shouldShowFooters(true) + adapter.setLayoutManager(gridLayoutManager) + binding.listRoot.layoutManager = gridLayoutManager + binding.listRoot.adapter = adapter + searchInCurrentDirectory(initialQuery ?: "") - withContext(Dispatchers.Main) { - adapter = UnifiedSearchListAdapter( - supportsOpeningCalendarContactsLocally(), - storageManager, - this@UnifiedSearchFragment, - this@UnifiedSearchFragment, - currentAccountProvider.user, - requireContext(), - viewThemeUtils, - appPreferences, - syncedFolderProvider, - client, - this@UnifiedSearchFragment - ) - - adapter.shouldShowFooters(true) - adapter.setLayoutManager(gridLayoutManager) - binding.listRoot.layoutManager = gridLayoutManager - binding.listRoot.adapter = adapter - searchInCurrentDirectory(initialQuery ?: "") - - setUpViewModel() - if (!initialQuery.isNullOrEmpty()) { - vm.setQuery(initialQuery!!) - vm.initialQuery() - } - } + setUpViewModel() + if (!initialQuery.isNullOrEmpty()) { + vm.setQuery(initialQuery!!) + vm.initialQuery() } } @@ -466,6 +466,26 @@ class UnifiedSearchFragment : vm.openResult(searchResultEntry) } + override fun loadFileThumbnail(searchResultEntry: SearchResultEntry, onClientReady: (NextcloudClient) -> Unit) { + client?.let { + onClientReady(it) + return + } + + lifecycleScope.launch(Dispatchers.IO) { + val newClient = getTypedActivity(FileActivity::class.java) + ?.clientRepository + ?.getNextcloudClient() + ?: return@launch + + client = newClient + + withContext(Dispatchers.Main) { + onClientReady(newClient) + } + } + } + override fun openFile(remotePath: String, showMoreActions: Boolean) { this.showMoreActions = showMoreActions vm.getRemoteFile(remotePath) 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 45a2a2831ee8..7848e262e19e 100644 --- a/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java +++ b/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java @@ -57,13 +57,13 @@ import com.owncloud.android.datamodel.ArbitraryDataProviderImpl; import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.datamodel.SyncedFolderProvider; import com.owncloud.android.datamodel.ThumbnailsCacheManager; import com.owncloud.android.lib.common.OwnCloudAccount; import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.lib.resources.files.model.ServerFileInterface; import com.owncloud.android.ui.TextDrawable; import com.owncloud.android.ui.dialog.SortingOrderDialogFragment; +import com.owncloud.android.utils.overlay.OverlayManager; import com.owncloud.android.utils.theme.ViewThemeUtils; import java.io.BufferedReader; @@ -80,10 +80,8 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; 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; @@ -111,12 +109,10 @@ public final class DisplayUtils { private static final String[] sizeSuffixes = {"B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}; private static final int[] sizeScales = {0, 0, 1, 1, 1, 2, 2, 2, 2}; - private static final String MIME_TYPE_UNKNOWN = "Unknown type"; private static final String HTTP_PROTOCOL = "http://"; private static final String HTTPS_PROTOCOL = "https://"; private static final String TWITTER_HANDLE_PREFIX = "@"; - private static final int MIMETYPE_PARTS_COUNT = 2; private static final int BYTE_SIZE_DIVIDER = 1024; private static final double BYTE_SIZE_DIVIDER_DOUBLE = 1024.0; private static final int DATE_TIME_PARTS_SIZE = 2; @@ -124,24 +120,6 @@ public final class DisplayUtils { public static final String MONTH_YEAR_PATTERN = "MMMM yyyy"; public static final String MONTH_PATTERN = "MMMM"; public static final String YEAR_PATTERN = "yyyy"; - public static final int SVG_SIZE = 512; - - private static Map mimeType2HumanReadable; - - static { - mimeType2HumanReadable = new HashMap<>(); - // images - mimeType2HumanReadable.put("image/jpeg", "JPEG image"); - mimeType2HumanReadable.put("image/jpg", "JPEG image"); - mimeType2HumanReadable.put("image/png", "PNG image"); - mimeType2HumanReadable.put("image/bmp", "Bitmap image"); - mimeType2HumanReadable.put("image/gif", "GIF image"); - mimeType2HumanReadable.put("image/svg+xml", "JPEG image"); - mimeType2HumanReadable.put("image/tiff", "TIFF image"); - // music - mimeType2HumanReadable.put("audio/mpeg", "MP3 music file"); - mimeType2HumanReadable.put("application/ogg", "OGG music file"); - } private DisplayUtils() { // utility class -> private constructor @@ -174,24 +152,6 @@ public static String bytesToHumanReadable(long bytes) { } } - /** - * Converts MIME types like "image/jpg" to more end user friendly output - * like "JPG image". - * - * @param mimetype MIME type to convert - * @return A human friendly version of the MIME type, {@link #MIME_TYPE_UNKNOWN} if it can't be converted - */ - public static String convertMIMEtoPrettyPrint(String mimetype) { - final String humanReadableMime = mimeType2HumanReadable.get(mimetype); - if (humanReadableMime != null) { - return humanReadableMime; - } - if (mimetype.split("/").length >= MIMETYPE_PARTS_COUNT) { - return mimetype.split("/")[1].toUpperCase(Locale.getDefault()) + " file"; - } - return MIME_TYPE_UNKNOWN; - } - /** * Converts Unix time to human readable format * @@ -356,21 +316,6 @@ public static CharSequence getRelativeDateTimeString(Context c, } } - /** - * Update the passed path removing the last "/" if it is not the root folder. - * - * @param path the path to be trimmed - */ - public static String getPathWithoutLastSlash(String path) { - - // Remove last slash from path - if (path.length() > 1 && path.charAt(path.length() - 1) == OCFile.PATH_SEPARATOR.charAt(0)) { - return path.substring(0, path.length() - 1); - } - - return path; - } - /** * Gets the screen size in pixels. * @@ -843,7 +788,7 @@ public static void setThumbnail(OCFile file, LoaderImageView shimmerThumbnail, AppPreferences preferences, ViewThemeUtils viewThemeUtils, - SyncedFolderProvider syncedFolderProvider) { + OverlayManager overlayManager) { if (file == null || thumbnailView == null || context == null) { return; } @@ -854,7 +799,7 @@ public static void setThumbnail(OCFile file, } if (file.isFolder()) { - setThumbnailForFolder(file, thumbnailView, shimmerThumbnail, user, syncedFolderProvider, preferences, context, viewThemeUtils); + overlayManager.setFolderThumbnail(file, thumbnailView, shimmerThumbnail); return; } @@ -899,17 +844,6 @@ private static void setThumbnailForOfflineOperation(OCFile file, ImageView thumb } } - private static void setThumbnailForFolder(OCFile file, ImageView thumbnailView, LoaderImageView shimmerThumbnail, User user, SyncedFolderProvider syncedFolderProvider, AppPreferences preferences, Context context, ViewThemeUtils viewThemeUtils) { - stopShimmer(shimmerThumbnail, thumbnailView); - - boolean isAutoUploadFolder = SyncedFolderProvider.isAutoUploadFolder(syncedFolderProvider, file, user); - boolean isDarkModeActive = preferences.isDarkModeEnabled(); - - final var overlayIconId = file.getFileOverlayIconId(isAutoUploadFolder); - final var fileIcon = MimeTypeUtil.getFolderIcon(isDarkModeActive, overlayIconId, context, viewThemeUtils); - thumbnailView.setImageDrawable(fileIcon); - } - private static void setThumbnailFromCache(OCFile file, ImageView thumbnailView, FileDataStorageManager storageManager, List asyncTasks, boolean gridView, LoaderImageView shimmerThumbnail, User user, AppPreferences preferences, Context context, ViewThemeUtils viewThemeUtils) { final var thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache(ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.getRemoteId()); if (thumbnail == null || file.isUpdateThumbnailNeeded()) { diff --git a/app/src/main/java/com/owncloud/android/utils/overlay/OverlayManager.kt b/app/src/main/java/com/owncloud/android/utils/overlay/OverlayManager.kt new file mode 100644 index 000000000000..4005a3435828 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/utils/overlay/OverlayManager.kt @@ -0,0 +1,103 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.utils.overlay + +import android.content.Context +import android.view.View +import android.widget.ImageView +import androidx.core.content.ContextCompat +import com.elyeproj.loaderviewlibrary.LoaderImageView +import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.di.Injectable +import com.nextcloud.client.preferences.AppPreferences +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.datamodel.SyncedFolderProvider +import com.owncloud.android.utils.DisplayUtils +import com.owncloud.android.utils.MimeTypeUtil +import com.owncloud.android.utils.theme.ViewThemeUtils +import javax.inject.Inject + +class OverlayManager @Inject constructor( + private val syncedFolderProvider: SyncedFolderProvider, + private val preferences: AppPreferences, + private val viewThemeUtils: ViewThemeUtils, + private val context: Context, + private val accountManager: UserAccountManager +) : Injectable { + + /** + * Sets the overlay icon for a folder into the provided [ImageView]. + * + * The icon is only applied when: + * - The [folder] is not null + * - The [folder] represents a directory + * - A valid overlay icon resource can be resolved + * + * The overlay icon depends on whether the folder is configured + * as an auto-upload folder for the current user. + * + * @param folder The [OCFile] representing the folder. + * @param imageView The [ImageView] where the overlay icon will be displayed. + */ + fun setFolderOverlayIcon(folder: OCFile?, imageView: ImageView) { + val overlayIconId = folder + ?.takeIf { it.isFolder } + ?.let { currentFolder -> + val isAutoUploadFolder = SyncedFolderProvider.isAutoUploadFolder( + syncedFolderProvider, + currentFolder, + accountManager.user + ) + currentFolder.getFileOverlayIconId(isAutoUploadFolder) + } + + if (overlayIconId == null) { + imageView.visibility = View.GONE + } else { + imageView.visibility = View.VISIBLE + imageView.setImageDrawable(ContextCompat.getDrawable(context, overlayIconId)) + } + } + + /** + * Sets the thumbnail for a folder into the provided [ImageView]. + * + * This method: + * - Ensures the given [folder] is not null and represents a directory. + * - Stops any active shimmer/loading animation on [loaderImageView]. + * - Resolves whether the folder is configured as an auto-upload folder + * for the current user. + * - Detects whether dark mode is currently enabled. + * - Retrieves the appropriate folder icon and overlay. + * + * The final drawable is created via `MimeTypeUtil.getFolderIcon(...)`, + * which returns a LayerDrawable. This drawable is built programmatically + * by stacking multiple layers (e.g., base folder icon + optional overlay icon) + * on top of each other, so everything is rendered inside a single [ImageView]. + * + * @param folder The [OCFile] representing the folder. + * @param imageView The [ImageView] where the composed folder thumbnail + * will be displayed. + * @param loaderImageView Optional [LoaderImageView] used for shimmer/loading + * state handling. If provided, its shimmer animation will be stopped before + * applying the final icon. + */ + fun setFolderThumbnail(folder: OCFile?, imageView: ImageView, loaderImageView: LoaderImageView?) { + if (folder == null || !folder.isFolder) return + + DisplayUtils.stopShimmer(loaderImageView, imageView) + + val isAutoUploadFolder = + SyncedFolderProvider.isAutoUploadFolder(syncedFolderProvider, folder, accountManager.user) + val isDarkModeActive = preferences.isDarkModeEnabled() + + val overlayIconId = folder.getFileOverlayIconId(isAutoUploadFolder) + val icon = MimeTypeUtil.getFolderIcon(isDarkModeActive, overlayIconId, context, viewThemeUtils) + imageView.setImageDrawable(icon) + } +} diff --git a/app/src/main/res/layout/unified_search_item.xml b/app/src/main/res/layout/unified_search_item.xml index 9f65a55268ca..ab8cf4038490 100755 --- a/app/src/main/res/layout/unified_search_item.xml +++ b/app/src/main/res/layout/unified_search_item.xml @@ -39,6 +39,18 @@ android:contentDescription="@null" android:src="@drawable/folder" /> + +