diff --git a/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt index 60fc48c23571..06a94aaafdff 100644 --- a/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt +++ b/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt @@ -99,7 +99,7 @@ interface FileDao { ORDER BY ${ProviderTableMeta.FILE_DEFAULT_SORT_ORDER} """ ) - fun getSubfiles( + suspend fun getSubfiles( parentId: Long, accountName: String, dirType: String = MimeType.DIRECTORY, diff --git a/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadWorker.kt index e160e1dd4558..a15dda964192 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadWorker.kt @@ -22,7 +22,7 @@ import androidx.work.WorkerParameters import com.nextcloud.client.account.User import com.nextcloud.client.account.UserAccountManager import com.nextcloud.model.WorkerState -import com.nextcloud.model.WorkerStateLiveData +import com.nextcloud.model.WorkerStateObserver import com.nextcloud.utils.ForegroundServiceHelper import com.nextcloud.utils.extensions.getPercent import com.owncloud.android.R @@ -152,11 +152,11 @@ class FileDownloadWorker( ) private fun setWorkerState(user: User?) { - WorkerStateLiveData.instance().setWorkState(WorkerState.DownloadStarted(user, currentDownload)) + WorkerStateObserver.send(WorkerState.FileDownloadStarted(user, currentDownload)) } private fun setIdleWorkerState() { - WorkerStateLiveData.instance().setWorkState(WorkerState.DownloadFinished(getCurrentFile())) + WorkerStateObserver.send(WorkerState.FileDownloadCompleted(getCurrentFile())) } private fun removePendingDownload(accountName: String?) { diff --git a/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorker.kt index 1ef24562c165..1f27c04f47e4 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorker.kt @@ -12,6 +12,8 @@ import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.jobs.download.FileDownloadHelper +import com.nextcloud.model.WorkerState +import com.nextcloud.model.WorkerStateObserver import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile import com.owncloud.android.lib.common.OwnCloudClientManagerFactory @@ -132,6 +134,7 @@ class FolderDownloadWorker( Log_OC.d(TAG, "❌ failed reason: $e") Result.failure() } finally { + WorkerStateObserver.send(WorkerState.FolderDownloadCompleted(folder)) pendingDownloads.remove(folder.fileId) notificationManager?.dismiss() } diff --git a/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/OfflineOperationsWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/OfflineOperationsWorker.kt index 9094918072e2..64b83867e827 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/OfflineOperationsWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/OfflineOperationsWorker.kt @@ -17,7 +17,7 @@ import com.nextcloud.client.network.ClientFactoryImpl import com.nextcloud.client.network.ConnectivityService import com.nextcloud.model.OfflineOperationType import com.nextcloud.model.WorkerState -import com.nextcloud.model.WorkerStateLiveData +import com.nextcloud.model.WorkerStateObserver import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile import com.owncloud.android.lib.common.OwnCloudClient @@ -85,7 +85,7 @@ class OfflineOperationsWorker( processOperations(operations, client) // finish - WorkerStateLiveData.instance().setWorkState(WorkerState.OfflineOperationsCompleted) + WorkerStateObserver.send(WorkerState.OfflineOperationsCompleted) Log_OC.d(TAG, "🏁 Worker finished with result") return@withContext Result.success() } catch (e: Exception) { diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt index c29f9c6a63d9..ca8562fe401f 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt @@ -22,7 +22,7 @@ import com.nextcloud.client.jobs.utils.UploadErrorNotificationManager import com.nextcloud.client.network.ConnectivityService import com.nextcloud.client.preferences.AppPreferences import com.nextcloud.model.WorkerState -import com.nextcloud.model.WorkerStateLiveData +import com.nextcloud.model.WorkerStateObserver import com.nextcloud.utils.ForegroundServiceHelper import com.nextcloud.utils.extensions.getPercent import com.nextcloud.utils.extensions.updateStatus @@ -186,11 +186,11 @@ class FileUploadWorker( } private fun setWorkerState(user: User?) { - WorkerStateLiveData.instance().setWorkState(WorkerState.UploadStarted(user)) + WorkerStateObserver.send(WorkerState.FileUploadStarted(user)) } private fun setIdleWorkerState() { - WorkerStateLiveData.instance().setWorkState(WorkerState.UploadFinished(currentUploadFileOperation?.file)) + WorkerStateObserver.send(WorkerState.FileUploadCompleted(currentUploadFileOperation?.file)) } @Suppress("ReturnCount", "LongMethod") diff --git a/app/src/main/java/com/nextcloud/model/WorkerState.kt b/app/src/main/java/com/nextcloud/model/WorkerState.kt index 6a1cca1330f0..7ed07584ac2d 100644 --- a/app/src/main/java/com/nextcloud/model/WorkerState.kt +++ b/app/src/main/java/com/nextcloud/model/WorkerState.kt @@ -12,9 +12,13 @@ import com.owncloud.android.datamodel.OCFile import com.owncloud.android.operations.DownloadFileOperation sealed class WorkerState { - data class DownloadFinished(var currentFile: OCFile?) : WorkerState() - data class DownloadStarted(var user: User?, var currentDownload: DownloadFileOperation?) : WorkerState() - data class UploadFinished(var currentFile: OCFile?) : WorkerState() - data class UploadStarted(var user: User?) : WorkerState() + data class FolderDownloadCompleted(var folder: OCFile) : WorkerState() + + data class FileDownloadStarted(var user: User?, var currentDownload: DownloadFileOperation?) : WorkerState() + data class FileDownloadCompleted(var currentFile: OCFile?) : WorkerState() + + data class FileUploadStarted(var user: User?) : WorkerState() + data class FileUploadCompleted(var currentFile: OCFile?) : WorkerState() + data object OfflineOperationsCompleted : WorkerState() } diff --git a/app/src/main/java/com/nextcloud/model/WorkerStateLiveData.kt b/app/src/main/java/com/nextcloud/model/WorkerStateLiveData.kt deleted file mode 100644 index 28d92b826752..000000000000 --- a/app/src/main/java/com/nextcloud/model/WorkerStateLiveData.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2023 Alper Ozturk - * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.nextcloud.model - -import androidx.lifecycle.LiveData - -class WorkerStateLiveData private constructor() : LiveData() { - - fun setWorkState(state: WorkerState) { - postValue(state) - } - - companion object { - private var instance: WorkerStateLiveData? = null - - fun instance(): WorkerStateLiveData = instance ?: synchronized(this) { - instance ?: WorkerStateLiveData().also { instance = it } - } - } -} diff --git a/app/src/main/java/com/nextcloud/model/WorkerStateObserver.kt b/app/src/main/java/com/nextcloud/model/WorkerStateObserver.kt new file mode 100644 index 000000000000..f8623f3b440c --- /dev/null +++ b/app/src/main/java/com/nextcloud/model/WorkerStateObserver.kt @@ -0,0 +1,22 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package com.nextcloud.model + +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow + +object WorkerStateObserver { + private const val BUFFER_CAPACITY = 25 + + private val _events = MutableSharedFlow(extraBufferCapacity = BUFFER_CAPACITY) + val events = _events.asSharedFlow() + + fun send(state: WorkerState) { + _events.tryEmit(state) + } +} diff --git a/app/src/main/java/com/nextcloud/utils/extensions/ActivityExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/ActivityExtensions.kt index 19c96861e726..c02e4e9f95f7 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/ActivityExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/ActivityExtensions.kt @@ -9,8 +9,15 @@ package com.nextcloud.utils.extensions import android.app.Activity import android.content.Intent +import androidx.activity.ComponentActivity import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.nextcloud.model.WorkerState +import com.nextcloud.model.WorkerStateObserver +import kotlinx.coroutines.launch fun AppCompatActivity.isDialogFragmentReady(fragment: Fragment): Boolean = isActive() && !fragment.isStateSaved @@ -29,3 +36,13 @@ fun Activity.showShareIntent(text: String?) { val shareIntent = Intent.createChooser(sendIntent, null) startActivity(shareIntent) } + +fun ComponentActivity.observeWorker(onCollect: (WorkerState?) -> Unit) { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + WorkerStateObserver.events.collect { + onCollect(it) + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt index 6567d7c5b559..77f662d76c11 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt @@ -44,7 +44,7 @@ fun FileDataStorageManager.getDecryptedPath(file: OCFile): String { .joinToString(OCFile.PATH_SEPARATOR) } -fun FileDataStorageManager.getSubfiles(id: Long, accountName: String): List = +suspend fun FileDataStorageManager.getSubfiles(id: Long, accountName: String): List = fileDao.getSubfiles(id, accountName).map { createFileInstance(it) } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt index d225ba4bff1d..3bbd6ab36235 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt @@ -47,7 +47,6 @@ import androidx.appcompat.widget.SearchView import androidx.core.view.MenuItemCompat import androidx.core.view.isVisible import androidx.fragment.app.Fragment -import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import androidx.localbroadcastmanager.content.LocalBroadcastManager import com.google.android.material.appbar.AppBarLayout @@ -75,16 +74,16 @@ import com.nextcloud.client.utils.IntentUtil import com.nextcloud.model.ToolbarItem import com.nextcloud.model.ToolbarStyle import com.nextcloud.model.WorkerState -import com.nextcloud.model.WorkerState.DownloadFinished -import com.nextcloud.model.WorkerState.DownloadStarted +import com.nextcloud.model.WorkerState.FileDownloadCompleted +import com.nextcloud.model.WorkerState.FileDownloadStarted import com.nextcloud.model.WorkerState.OfflineOperationsCompleted -import com.nextcloud.model.WorkerState.UploadFinished -import com.nextcloud.model.WorkerStateLiveData +import com.nextcloud.model.WorkerState.FileUploadCompleted import com.nextcloud.utils.extensions.getParcelableArgument import com.nextcloud.utils.extensions.isActive import com.nextcloud.utils.extensions.lastFragment import com.nextcloud.utils.extensions.logFileSize import com.nextcloud.utils.extensions.navigateToAllFiles +import com.nextcloud.utils.extensions.observeWorker import com.nextcloud.utils.fileNameValidator.FileNameValidator.checkFolderPath import com.nextcloud.utils.view.FastScrollUtils import com.owncloud.android.MainApp @@ -1875,36 +1874,38 @@ class FileDisplayActivity : override fun isDrawerIndicatorAvailable(): Boolean = isRoot(getCurrentDir()) private fun observeWorkerState() { - WorkerStateLiveData.Companion.instance().observe( - this, - Observer { state: WorkerState? -> - when (state) { - is DownloadStarted -> { - Log_OC.d(TAG, "Download worker started") - handleDownloadWorkerState() - } + observeWorker { state -> + when (state) { + is FileDownloadStarted -> { + Log_OC.d(TAG, "Download worker started") + handleDownloadWorkerState() + } - is DownloadFinished -> { - fileDownloadProgressListener = null - previewFile(state) - } + is FileDownloadCompleted -> { + fileDownloadProgressListener = null + previewFile(state) + } - is UploadFinished -> { - refreshList() + is FileUploadCompleted -> { + state.currentFile?.let { + ocFileListFragment?.adapter?.insertFile(it) } + } - is OfflineOperationsCompleted -> { - refreshCurrentDirectory() - } + is OfflineOperationsCompleted -> { + refreshCurrentDirectory() + } - else -> { - } + is WorkerState.FolderDownloadCompleted -> { + ocFileListFragment?.adapter?.notifyItemChanged(state.folder) } + + else -> Unit } - ) + } } - private fun previewFile(finishedState: DownloadFinished) { + private fun previewFile(finishedState: FileDownloadCompleted) { if (fileIDForImmediatePreview == -1L) { return } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ManageAccountsActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/ManageAccountsActivity.kt index a1357e16170c..79bd72d76099 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/ManageAccountsActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/ManageAccountsActivity.kt @@ -33,9 +33,9 @@ import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.jobs.download.FileDownloadHelper import com.nextcloud.client.onboarding.FirstRunActivity import com.nextcloud.model.WorkerState -import com.nextcloud.model.WorkerState.DownloadStarted -import com.nextcloud.model.WorkerStateLiveData +import com.nextcloud.model.WorkerState.FileDownloadStarted import com.nextcloud.utils.extensions.getParcelableArgument +import com.nextcloud.utils.extensions.observeWorker import com.nextcloud.utils.mdm.MDMConfig.multiAccountSupport import com.owncloud.android.MainApp import com.owncloud.android.R @@ -454,10 +454,8 @@ class ManageAccountsActivity : } private fun observeWorkerState() { - WorkerStateLiveData.instance().observe( - this - ) { state: WorkerState? -> - if (state is DownloadStarted) { + observeWorker { state: WorkerState? -> + if (state is FileDownloadStarted) { Log_OC.d(TAG, "Download worker started") workerAccountName = state.user?.accountName workerCurrentDownload = state.currentDownload diff --git a/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java index 8d61cbdcac2f..f66a279a44a2 100755 --- a/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java @@ -29,7 +29,7 @@ import com.nextcloud.client.jobs.upload.FileUploadWorker; import com.nextcloud.client.utils.Throttler; import com.nextcloud.model.WorkerState; -import com.nextcloud.model.WorkerStateLiveData; +import com.nextcloud.utils.extensions.ActivityExtensionsKt; import com.owncloud.android.R; import com.owncloud.android.databinding.UploadListLayoutBinding; import com.owncloud.android.datamodel.OCFile; @@ -49,6 +49,7 @@ import androidx.recyclerview.widget.GridLayoutManager; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import kotlin.Unit; /** * Activity listing pending, active, and completed uploads. User can delete completed uploads from view. Content of this @@ -129,13 +130,14 @@ protected void onCreate(Bundle savedInstanceState) { } private void observeWorkerState() { - WorkerStateLiveData.Companion.instance().observe(this, state -> { - if (state instanceof WorkerState.UploadStarted) { + ActivityExtensionsKt.observeWorker(this, state -> { + if (state instanceof WorkerState.FileUploadStarted) { Log_OC.d(TAG, "Upload worker started"); uploadListAdapter.loadUploadItemsFromDb(); - } else if (state instanceof WorkerState.UploadFinished) { + } else if (state instanceof WorkerState.FileUploadCompleted) { uploadListAdapter.loadUploadItemsFromDb(() -> swipeListRefreshLayout.setRefreshing(false)); } + return Unit.INSTANCE; }); } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/ListViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/ListViewHolder.kt index 44e9fa7e8ee8..cceefd293dbf 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/ListViewHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/ListViewHolder.kt @@ -30,4 +30,5 @@ interface ListViewHolder { val gridLivePhotoIndicator: ImageView? val livePhotoIndicator: TextView? val livePhotoIndicatorSeparator: TextView? + val hasVisibleFeatureIndicators: Boolean } 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 579cd3120f68..0af042ff685a 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,7 +25,6 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; -import android.widget.LinearLayout; import com.elyeproj.loaderviewlibrary.LoaderImageView; import com.google.android.material.chip.Chip; @@ -467,7 +466,7 @@ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int positi } } else { - ListViewHolder gridViewHolder = (ListViewHolder) holder; + ListViewHolder viewHolder = (ListViewHolder) holder; OCFile file = getItem(position); if (file == null) { @@ -475,7 +474,7 @@ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int positi return; } - bindHolder(holder, gridViewHolder, file); + bindHolder(holder, viewHolder, file); } } @@ -483,9 +482,8 @@ public void bindRecommendedFilesHolder(OCFileListRecommendedItemViewHolder holde bindHolder(holder, holder, file); } - private void bindHolder(@NonNull RecyclerView.ViewHolder holder, ListViewHolder gridViewHolder, OCFile file) { - ocFileListDelegate.bindGridViewHolder(gridViewHolder, file, currentDirectory, searchType); - checkVisibilityOfFileFeaturesLayout(gridViewHolder); + private void bindHolder(@NonNull RecyclerView.ViewHolder holder, ListViewHolder viewHolder, OCFile file) { + ocFileListDelegate.bindViewHolder(viewHolder, file, currentDirectory, searchType); if (holder instanceof ListItemViewHolder itemViewHolder) { bindListItemViewHolder(itemViewHolder, file); @@ -493,16 +491,20 @@ private void bindHolder(@NonNull RecyclerView.ViewHolder holder, ListViewHolder if (holder instanceof ListGridItemViewHolder gridItemViewHolder) { setFilenameAndExtension(gridItemViewHolder, file); - checkVisibilityOfFileFeaturesLayout(gridItemViewHolder); } - updateLivePhotoIndicators(gridViewHolder, file); + updateLivePhotoIndicators(viewHolder, file); if (!MDMConfig.INSTANCE.sharingSupport(activity)) { - gridViewHolder.getShared().setVisibility(View.GONE); + viewHolder.getShared().setVisibility(View.GONE); } - setVisibilityOfMoreOption(gridViewHolder); + setVisibilityOfMoreOption(viewHolder); + + final var fileFeatureLayout = viewHolder.getFileFeaturesLayout(); + if (fileFeatureLayout != null && viewHolder.getHasVisibleFeatureIndicators()) { + fileFeatureLayout.setVisibility(View.VISIBLE); + } } private boolean shouldShowRecommendedFiles() { @@ -518,24 +520,6 @@ private boolean shouldShowOpenInNotes() { return notesFolderPath != null && currentPath != null && currentPath.startsWith(notesFolderPath); } - private void checkVisibilityOfFileFeaturesLayout(ListViewHolder holder) { - int fileFeaturesVisibility = View.GONE; - LinearLayout fileFeaturesLayout = holder.getFileFeaturesLayout(); - - if (fileFeaturesLayout == null) { - return; - } - - for (int i = 0; i < fileFeaturesLayout.getChildCount(); i++) { - View child = fileFeaturesLayout.getChildAt(i); - if (child.getVisibility() == View.VISIBLE) { - fileFeaturesVisibility = View.VISIBLE; - } - } - - fileFeaturesLayout.setVisibility(fileFeaturesVisibility); - } - private void updateLivePhotoIndicators(ListViewHolder holder, OCFile file) { boolean isLivePhoto = file.getLinkedFileIdForLivePhoto() != null; @@ -701,8 +685,6 @@ private void bindListItemViewHolder(ListItemViewHolder holder, OCFile file) { } else { holder.getOverflowMenu().setImageResource(R.drawable.ic_dots_vertical); } - - setVisibilityOfMoreOption(holder); } private void setVisibilityOfMoreOption(Object holder) { @@ -1075,4 +1057,24 @@ public void cleanup() { ocFileListDelegate.cleanup(); helper.cleanup(); } + + public void insertFile(@NonNull OCFile file) { + mFiles.add(file); + mFilesAll.add(file); + + // Re-sort to maintain order + if (sortOrder != null) { + boolean foldersBeforeFiles = preferences.isSortFoldersBeforeFiles(); + boolean favoritesFirst = preferences.isSortFavoritesFirst(); + mFiles = sortOrder.sortCloudFiles(mFiles, foldersBeforeFiles, favoritesFirst); + } + + // Find actual position and notify + int position = mFiles.indexOf(file); + if (shouldShowHeader()) { + position++; + } + + notifyItemInserted(position); + } } 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 4190f148ba9e..7cc6c4b59e6f 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 @@ -41,6 +41,7 @@ import com.owncloud.android.utils.EncryptionUtils import com.owncloud.android.utils.theme.ViewThemeUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -66,7 +67,7 @@ class OCFileListDelegate( private var highlightedItem: OCFile? = null var isMultiSelect = false private val asyncTasks: MutableList = ArrayList() - private val ioScope = CoroutineScope(Dispatchers.IO) + private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val galleryImageGenerationJob = GalleryImageGenerationJob(user, storageManager) fun setHighlightedItem(highlightedItem: OCFile?) { @@ -192,32 +193,27 @@ class OCFileListDelegate( } @Suppress("MagicNumber") - fun bindGridViewHolder( - gridViewHolder: ListViewHolder, - file: OCFile, - currentDirectory: OCFile?, - searchType: SearchType? - ) { + fun bindViewHolder(viewHolder: ListViewHolder, file: OCFile, currentDirectory: OCFile?, searchType: SearchType?) { // thumbnail - gridViewHolder.imageFileName?.text = file.fileName - gridViewHolder.thumbnail.tag = file.fileId - setThumbnail(gridViewHolder.thumbnail, gridViewHolder.shimmerThumbnail, file) + viewHolder.imageFileName?.text = file.fileName + viewHolder.thumbnail.tag = file.fileId + setThumbnail(viewHolder.thumbnail, viewHolder.shimmerThumbnail, file) // item layout + click listeners - bindGridItemLayout(file, gridViewHolder) + bindGridItemLayout(file, viewHolder) // unread comments - bindUnreadComments(file, gridViewHolder) + bindUnreadComments(file, viewHolder) // multiSelect (Checkbox) val isFolderPickerActivity = (context is FolderPickerActivity) - gridViewHolder.checkbox.setVisibleIf(isMultiSelect && !isFolderPickerActivity) + viewHolder.checkbox.setVisibleIf(isMultiSelect && !isFolderPickerActivity) // download state - gridViewHolder.localFileIndicator.visibility = View.GONE // default first + viewHolder.localFileIndicator.visibility = View.GONE // default first // metadata (downloaded, favorite) - bindGridMetadataViews(file, gridViewHolder) + bindGridMetadataViews(file, viewHolder) // shares val shouldHideShare = ( @@ -232,13 +228,13 @@ class OCFileListDelegate( currentDirectory?.isEncrypted ?: false ) // sharing an encrypted subfolder is not possible if (shouldHideShare) { - gridViewHolder.shared.visibility = View.GONE + viewHolder.shared.visibility = View.GONE } else { - configureSharedIconView(gridViewHolder, file) + configureSharedIconView(viewHolder, file) } if (!file.isOfflineOperation && !file.isFolder) { - gridViewHolder.thumbnail.makeRounded(context, 4f) + viewHolder.thumbnail.makeRounded(context, 4f) } } @@ -321,19 +317,11 @@ class OCFileListDelegate( } } - @Suppress("ReturnCount") - private fun isFolderFullyDownloaded(file: OCFile): Boolean { - if (!file.isFolder) { - return false - } - - val subfiles = storageManager.getSubfiles(file.fileId, user.accountName) - - if (subfiles.isEmpty()) { - return false - } - - return subfiles.all { it.isDown } + private suspend fun isFolderFullyDownloaded(file: OCFile): Boolean = withContext(Dispatchers.IO) { + file.isFolder && + storageManager.getSubfiles(file.fileId, user.accountName) + .takeIf { it.isNotEmpty() } + ?.all { it.isDown } == true } private fun isSynchronizing(file: OCFile): Boolean { @@ -346,36 +334,26 @@ class OCFileListDelegate( } private fun showLocalFileIndicator(file: OCFile, holder: ListViewHolder) { - val icon = when { - isSynchronizing(file) -> R.drawable.ic_synchronizing - file.etagInConflict != null -> R.drawable.ic_synchronizing_error - file.isDown -> R.drawable.ic_synced - else -> null - } - - holder.localFileIndicator.run { - if (icon != null) { - setImageResource(icon) - visibility = View.VISIBLE - } else { - visibility = View.GONE + ioScope.launch { + val isFullyDownloaded = isFolderFullyDownloaded(file) + val isSyncing = isSynchronizing(file) + val hasConflict = (file.etagInConflict != null) + val isDown = file.isDown + + val icon = when { + isSyncing -> R.drawable.ic_synchronizing + hasConflict -> R.drawable.ic_synchronizing_error + isDown || isFullyDownloaded -> R.drawable.ic_synced + else -> null } - } - checkLocalFolderIndicatorAsynchronously(file, holder) - } - - private fun checkLocalFolderIndicatorAsynchronously(file: OCFile, holder: ListViewHolder) { - if (file.isFolder) { - ioScope.launch { - if (isFolderFullyDownloaded(file)) { - withContext(Dispatchers.Main) { - holder.run { - if (thumbnail.tag == file.fileId) { - localFileIndicator.setImageResource(R.drawable.ic_synced) - localFileIndicator.visibility = View.VISIBLE - } - } + withContext(Dispatchers.Main) { + holder.localFileIndicator.run { + if (icon != null && showMetadata) { + setImageResource(icon) + visibility = View.VISIBLE + } else { + visibility = View.GONE } } } 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 index dc00b032e3c7..484bf8f62622 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridItemViewHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridItemViewHolder.kt @@ -62,6 +62,9 @@ class OCFileListGridItemViewHolder(var binding: GridItemBinding) : get() = null override val livePhotoIndicatorSeparator: TextView? get() = null + override val hasVisibleFeatureIndicators: Boolean + get() = localFileIndicator.isVisible || gridLivePhotoIndicator?.isVisible == true || unreadComments.isVisible || + shared.isVisible || binding.videoOverlay.isVisible || favorite.isVisible override val fileFeaturesLayout: LinearLayout get() = binding.fileFeaturesLayout override val more: ImageButton 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 index d11e13818f2a..3336b2b62663 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListItemViewHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListItemViewHolder.kt @@ -30,6 +30,8 @@ class OCFileListItemViewHolder(private var binding: ListItemBinding) : get() = binding.livePhotoIndicator override val livePhotoIndicatorSeparator: TextView get() = binding.livePhotoIndicatorSeparator + override val hasVisibleFeatureIndicators: Boolean + get() = false override val fileSize: TextView get() = binding.fileSize diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListRecommendedItemViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListRecommendedItemViewHolder.kt index b5f4b3368d52..9d805087a627 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListRecommendedItemViewHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListRecommendedItemViewHolder.kt @@ -12,6 +12,7 @@ import android.widget.ImageButton import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView +import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import com.elyeproj.loaderviewlibrary.LoaderImageView import com.owncloud.android.databinding.RecommendedFileItemBinding @@ -38,6 +39,9 @@ class OCFileListRecommendedItemViewHolder(private val binding: RecommendedFileIt override val gridLivePhotoIndicator: ImageView? get() = null override val livePhotoIndicator: TextView? get() = null override val livePhotoIndicatorSeparator: TextView? get() = null + override val hasVisibleFeatureIndicators: Boolean + get() = localFileIndicator.isVisible || gridLivePhotoIndicator?.isVisible == true || unreadComments.isVisible || + shared.isVisible || binding.videoOverlay.isVisible || favorite.isVisible override fun showVideoOverlay() { binding.videoOverlay.visibility = View.VISIBLE diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListViewHolder.kt index b0a0c037cd3a..1de965ae1083 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListViewHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListViewHolder.kt @@ -65,6 +65,9 @@ internal class OCFileListViewHolder(var binding: GridItemBinding) : get() = null override val livePhotoIndicatorSeparator: TextView? get() = null + override val hasVisibleFeatureIndicators: Boolean + get() = localFileIndicator.isVisible || gridLivePhotoIndicator.isVisible || unreadComments.isVisible || + shared.isVisible || binding.videoOverlay.isVisible || favorite.isVisible init { binding.favoriteAction.drawable.mutate() diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java index 2aaa0b4f9139..e1990f7b652a 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java @@ -33,10 +33,10 @@ import com.nextcloud.client.network.ConnectivityService; import com.nextcloud.client.preferences.AppPreferences; import com.nextcloud.model.WorkerState; -import com.nextcloud.model.WorkerStateLiveData; import com.nextcloud.ui.fileactions.FileAction; import com.nextcloud.ui.fileactions.FileActionsBottomSheet; import com.nextcloud.utils.MenuUtils; +import com.nextcloud.utils.extensions.ActivityExtensionsKt; import com.nextcloud.utils.extensions.BundleExtensionsKt; import com.nextcloud.utils.extensions.FileExtensionsKt; import com.nextcloud.utils.mdm.MDMConfig; @@ -84,6 +84,7 @@ import androidx.core.content.res.ResourcesCompat; import androidx.fragment.app.FragmentManager; import androidx.viewpager2.widget.ViewPager2; +import kotlin.Unit; /** * This Fragment is used to display the details about a file. @@ -541,7 +542,7 @@ public void updateFileDetails(OCFile file, User user) { * * @param transferring Flag signaling if the file should be considered as downloading or uploading, although * {@link FileDownloadHelper#isDownloading(User, OCFile)} and - * {@link FileUploadHelper#isUploading(User, OCFile)} return false. + * {@link FileUploadHelper#isUploading(String, String)} return false. * @param refresh If 'true', try to refresh the whole file from the database */ public void updateFileDetails(boolean transferring, boolean refresh) { @@ -625,12 +626,13 @@ public void onDownloadProgress(FileDownloadProgressEvent event) { } private void observeWorkerState() { - WorkerStateLiveData.Companion.instance().observe(getViewLifecycleOwner(), state -> { - if (state instanceof WorkerState.UploadStarted) { + ActivityExtensionsKt.observeWorker(requireActivity(), state -> { + if (state instanceof WorkerState.FileUploadStarted) { binding.progressText.setText(R.string.uploader_upload_in_progress_ticker); } else { binding.progressBlock.setVisibility(View.GONE); } + return Unit.INSTANCE; }); } 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 2525e6de94eb..5e764d15c550 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 @@ -1697,39 +1697,45 @@ public void switchToGridView() { @SuppressLint("NotifyDataSetChanged") public void switchLayoutManager(boolean grid) { + final var recyclerView = getRecyclerView(); + final var adapter = getAdapter(); + final var context = getContext(); + + if (context == null || adapter == null || recyclerView == null) { + Log_OC.e(TAG, "cannot switch layout, arguments are null"); + return; + } + int position = 0; - if (getRecyclerView() != null && getRecyclerView().getLayoutManager() != null) { - position = ((LinearLayoutManager) getRecyclerView().getLayoutManager()) - .findFirstCompletelyVisibleItemPosition(); + if (recyclerView.getLayoutManager() instanceof LinearLayoutManager linearLayoutManager) { + position = linearLayoutManager.findFirstCompletelyVisibleItemPosition(); } RecyclerView.LayoutManager layoutManager; if (grid) { - layoutManager = new GridLayoutManager(getContext(), getColumnsCount()); - ((GridLayoutManager) layoutManager).setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { + layoutManager = new GridLayoutManager(context, getColumnsCount()); + GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager; + gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { if (position == getAdapter().getItemCount() - 1 || position == 0 && getAdapter().shouldShowHeader()) { - return ((GridLayoutManager) layoutManager).getSpanCount(); + return gridLayoutManager.getSpanCount(); } else { return 1; } } }); - } else { - layoutManager = new LinearLayoutManager(getContext()); + layoutManager = new LinearLayoutManager(context); } - if (getRecyclerView() != null) { - getRecyclerView().setLayoutManager(layoutManager); - getRecyclerView().scrollToPosition(position); - getAdapter().setGridView(grid); - getRecyclerView().setAdapter(getAdapter()); - getAdapter().notifyDataSetChanged(); - } + recyclerView.setLayoutManager(layoutManager); + recyclerView.scrollToPosition(position); + adapter.setGridView(grid); + recyclerView.setAdapter(adapter); + adapter.notifyDataSetChanged(); } public CommonOCFileListAdapterInterface getCommonAdapter() { diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.kt b/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.kt index e45c42cd441f..f189c69a94ad 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.kt @@ -30,9 +30,9 @@ import com.nextcloud.client.jobs.download.FileDownloadWorker.Companion.getDownlo import com.nextcloud.client.jobs.upload.FileUploadWorker.Companion.getUploadFinishMessage import com.nextcloud.client.preferences.AppPreferences import com.nextcloud.model.WorkerState -import com.nextcloud.model.WorkerStateLiveData import com.nextcloud.utils.extensions.getParcelableArgument import com.nextcloud.utils.extensions.getSerializableArgument +import com.nextcloud.utils.extensions.observeWorker import com.owncloud.android.MainApp import com.owncloud.android.R import com.owncloud.android.datamodel.FileDataStorageManager @@ -325,9 +325,9 @@ class PreviewImageActivity : } private fun observeWorkerState() { - WorkerStateLiveData.instance().observe(this) { state: WorkerState? -> + observeWorker { state: WorkerState? -> when (state) { - is WorkerState.DownloadStarted -> { + is WorkerState.FileDownloadStarted -> { Log_OC.d(TAG, "Download worker started") isDownloadWorkStarted = true @@ -336,7 +336,7 @@ class PreviewImageActivity : } } - is WorkerState.DownloadFinished -> { + is WorkerState.FileDownloadCompleted -> { Log_OC.d(TAG, "Download worker stopped") isDownloadWorkStarted = false diff --git a/app/src/main/res/layout/grid_item.xml b/app/src/main/res/layout/grid_item.xml index ae065d33d73a..ddbbc8c627c3 100644 --- a/app/src/main/res/layout/grid_item.xml +++ b/app/src/main/res/layout/grid_item.xml @@ -124,6 +124,7 @@ android:layout_marginEnd="@dimen/grid_layout_margin_end" android:contentDescription="@string/synced_icon" android:src="@drawable/ic_synced" + android:visibility="gone" tools:visibility="visible" /> diff --git a/app/src/main/res/layout/list_item.xml b/app/src/main/res/layout/list_item.xml index 84b865219115..8717f2ffc7aa 100644 --- a/app/src/main/res/layout/list_item.xml +++ b/app/src/main/res/layout/list_item.xml @@ -46,6 +46,8 @@ android:layout_width="@dimen/list_item_local_file_indicator_layout_width" android:layout_height="@dimen/list_item_local_file_indicator_layout_height" android:contentDescription="@string/downloader_download_succeeded_ticker" + android:visibility="gone" + tools:visibility="visible" android:scaleType="fitCenter" android:src="@drawable/ic_synced" app:layout_constraintBottom_toBottomOf="@+id/thumbnail_container" diff --git a/app/src/main/res/layout/recommended_file_item.xml b/app/src/main/res/layout/recommended_file_item.xml index 738cf5900cc3..0d91762076a2 100644 --- a/app/src/main/res/layout/recommended_file_item.xml +++ b/app/src/main/res/layout/recommended_file_item.xml @@ -119,6 +119,7 @@ android:layout_width="@dimen/grid_layout_item_size" android:layout_height="@dimen/grid_layout_item_size" android:layout_marginEnd="@dimen/grid_layout_margin_end" + android:visibility="gone" android:contentDescription="@string/synced_icon" android:src="@drawable/ic_synced" tools:visibility="visible" />