Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,308 changes: 1,308 additions & 0 deletions app/schemas/com.nextcloud.client.database.NextcloudDatabase/99.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,9 @@ import com.owncloud.android.db.ProviderMeta
AutoMigration(from = 93, to = 94, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class),
AutoMigration(from = 94, to = 95, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class),
AutoMigration(from = 95, to = 96),
AutoMigration(from = 96, to = 97, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class)
AutoMigration(from = 96, to = 97, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class),
// manual migration used for 97 to 98
AutoMigration(from = 98, to = 99)
],
exportSchema = true
)
Expand Down
17 changes: 17 additions & 0 deletions app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ package com.nextcloud.client.database.dao

import androidx.room.Dao
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import com.nextcloud.client.database.entity.FileEntity
import com.owncloud.android.db.ProviderMeta.ProviderTableMeta
Expand Down Expand Up @@ -146,4 +147,20 @@ interface FileDao {

@Query("SELECT remote_id FROM filelist WHERE file_owner = :accountName AND remote_id IS NOT NULL")
fun getAllRemoteIds(accountName: String): List<String>

@Transaction
fun updateFileIndicatorsBatch(updates: List<Pair<Long, Int?>>) {
updates.forEach { (fileId, indicator) ->
updateFileIndicator(fileId, indicator)
}
}

@Query(
"""
UPDATE ${ProviderTableMeta.FILE_TABLE_NAME}
SET ${ProviderTableMeta.FILE_INDICATOR} = :indicator
WHERE ${ProviderTableMeta._ID} = :fileId
"""
)
fun updateFileIndicator(fileId: Long, indicator: Int?)
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,5 +121,7 @@ data class FileEntity(
@ColumnInfo(name = ProviderTableMeta.FILE_INTERNAL_TWO_WAY_SYNC_RESULT)
val internalTwoWaySyncResult: String?,
@ColumnInfo(name = ProviderTableMeta.FILE_UPLOADED)
val uploaded: Long?
val uploaded: Long?,
@ColumnInfo(name = ProviderTableMeta.FILE_INDICATOR)
val fileIndicator: Int?
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

package com.nextcloud.client.files

import com.owncloud.android.R
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update

sealed class FileIndicator(val iconRes: Int?) {
data object Idle : FileIndicator(null)
data object Downloading : FileIndicator(R.drawable.ic_synchronizing)
data object Error : FileIndicator(R.drawable.ic_synchronizing_error)
data object Downloaded : FileIndicator(R.drawable.ic_synced)
}

object FileIndicatorManager {
private val _activeTransfers = MutableStateFlow<Map<Long, FileIndicator>>(emptyMap())
val activeTransfers: StateFlow<Map<Long, FileIndicator>> = _activeTransfers

fun update(fileId: Long, status: FileIndicator) {
_activeTransfers.update { current ->
current + (fileId to status)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters
import com.nextcloud.client.account.User
import com.nextcloud.client.account.UserAccountManager
import com.nextcloud.client.files.FileIndicator
import com.nextcloud.client.files.FileIndicatorManager
import com.nextcloud.model.WorkerState
import com.nextcloud.model.WorkerStateObserver
import com.nextcloud.utils.ForegroundServiceHelper
Expand Down Expand Up @@ -211,6 +213,7 @@ class FileDownloadWorker(
file.remotePath,
operation
) ?: Pair(null, null)
FileIndicatorManager.update(file.fileId, FileIndicator.Downloading)

downloadKey?.let {
requestedDownloads.add(downloadKey)
Expand Down Expand Up @@ -354,6 +357,7 @@ class FileDownloadWorker(

private fun checkDownloadError(result: RemoteOperationResult<*>) {
if (result.isSuccess || downloadError != null) {
currentDownload?.file?.fileId?.let { FileIndicatorManager.update(it, FileIndicator.Downloaded) }
notificationManager.dismissNotification()
return
}
Expand All @@ -363,6 +367,8 @@ class FileDownloadWorker(
} else {
FileDownloadError.Failed
}

currentDownload?.file?.fileId?.let { FileIndicatorManager.update(it, FileIndicator.Error) }
}

private fun showDownloadErrorNotification(downloadError: FileDownloadError) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters
import com.nextcloud.client.account.UserAccountManager
import com.nextcloud.client.files.FileIndicator
import com.nextcloud.client.files.FileIndicatorManager
import com.nextcloud.client.jobs.download.FileDownloadHelper
import com.nextcloud.model.WorkerState
import com.nextcloud.model.WorkerStateObserver
Expand Down Expand Up @@ -75,6 +77,7 @@ class FolderDownloadWorker(
return Result.failure()
}

FileIndicatorManager.update(folder.fileId, FileIndicator.Downloading)
Log_OC.d(TAG, "🕒 started for ${user.accountName} downloading ${folder.fileName}")

trySetForeground(folder)
Expand Down Expand Up @@ -108,9 +111,11 @@ class FolderDownloadWorker(
setForeground(foregroundInfo)
}

FileIndicatorManager.update(file.fileId, FileIndicator.Downloading)
val operation = DownloadFileOperation(user, file, context)
val operationResult = operation.execute(client)
if (operationResult?.isSuccess == true && operation.downloadType === DownloadType.DOWNLOAD) {
FileIndicatorManager.update(file.fileId, FileIndicator.Downloaded)
getOCFile(operation)?.let { ocFile ->
downloadHelper.saveFile(ocFile, operation, storageManager)
}
Expand All @@ -127,13 +132,16 @@ class FolderDownloadWorker(

if (result) {
Log_OC.d(TAG, "✅ completed")
FileIndicatorManager.update(folderID, FileIndicator.Downloaded)
Result.success()
} else {
Log_OC.d(TAG, "❌ failed")
FileIndicatorManager.update(folderID, FileIndicator.Error)
Result.failure()
}
} catch (e: Exception) {
Log_OC.d(TAG, "❌ failed reason: $e")
FileIndicatorManager.update(folderID, FileIndicator.Error)
Result.failure()
} finally {
WorkerStateObserver.send(WorkerState.FolderDownloadCompleted(folder))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,12 @@ public void saveFolder(OCFile folder, List<OCFile> updatedFiles, Collection<OCFi
@SuppressFBWarnings("CE")
private ContentValues createContentValuesBase(OCFile fileOrFolder) {
final ContentValues cv = new ContentValues();

Integer fileIndicator = fileOrFolder.getFileIndicator();
if (fileIndicator != null) {
cv.put(ProviderTableMeta.FILE_INDICATOR, fileIndicator);
}

cv.put(ProviderTableMeta.FILE_MODIFIED, fileOrFolder.getModificationTimestamp());
cv.put(ProviderTableMeta.FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA, fileOrFolder.getModificationTimestampAtLastSyncForData());
cv.put(ProviderTableMeta.FILE_PARENT, fileOrFolder.getParentId());
Expand Down Expand Up @@ -918,7 +924,6 @@ public boolean removeFile(OCFile ocFile, boolean removeDBData, boolean removeLoc
return success;
}


public boolean removeFolder(OCFile folder, boolean removeDBData, boolean removeLocalContent) {
boolean success = true;
if (folder != null && folder.isFolder()) {
Expand Down Expand Up @@ -1258,6 +1263,7 @@ public OCFile createFileInstance(FileEntity fileEntity) {
}
ocFile.setFileLength(nullToZero(fileEntity.getContentLength()));
ocFile.setUploadTimestamp(nullToZero(fileEntity.getUploaded()));
ocFile.setFileIndicator(nullToZero(fileEntity.getFileIndicator()));
ocFile.setCreationTimestamp(nullToZero(fileEntity.getCreation()));
ocFile.setModificationTimestamp(nullToZero(fileEntity.getModified()));
ocFile.setModificationTimestampAtLastSyncForData(nullToZero(fileEntity.getModifiedAtLastSyncForData()));
Expand Down
13 changes: 13 additions & 0 deletions app/src/main/java/com/owncloud/android/datamodel/OCFile.java
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ public class OCFile implements Parcelable, Comparable<OCFile>, ServerFileInterfa
private String reason = "";
// endregion

private Integer fileIndicator = null;

/**
* URI to the local path of the file contents, if stored in the device; cached after first call to
* {@link #getStorageUri()}
Expand Down Expand Up @@ -212,6 +214,7 @@ private OCFile(Parcel source) {
lockTimeout = source.readLong();
lockToken = source.readString();
livePhoto = source.readString();
fileIndicator = source.readInt();
}

@Override
Expand Down Expand Up @@ -258,6 +261,7 @@ public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(lockTimeout);
dest.writeString(lockToken);
dest.writeString(livePhoto);
dest.writeInt(fileIndicator != null ? fileIndicator : -1);
}

public String getLinkedFileIdForLivePhoto() {
Expand Down Expand Up @@ -530,6 +534,7 @@ private void resetData() {
lockToken = null;
livePhoto = null;
imageDimension = null;
fileIndicator = null;
}

/**
Expand Down Expand Up @@ -1175,4 +1180,12 @@ public boolean hasValidParentId() {
return getParentId() != 0;
}
}

public void setFileIndicator(Integer indicator) {
fileIndicator = indicator;
}

public Integer getFileIndicator() {
return fileIndicator;
}
}
6 changes: 4 additions & 2 deletions app/src/main/java/com/owncloud/android/db/ProviderMeta.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
*/
public class ProviderMeta {
public static final String DB_NAME = "filelist";
public static final int DB_VERSION = 98;
public static final int DB_VERSION = 99;

private ProviderMeta() {
// No instance
Expand Down Expand Up @@ -91,6 +91,7 @@ static public class ProviderTableMeta implements BaseColumns {
public static final String FILE_CREATION = "created";
public static final String FILE_MODIFIED = "modified";
public static final String FILE_UPLOADED = "uploaded";
public static final String FILE_INDICATOR = "file_indicator";
public static final String FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA = "modified_at_last_sync_for_data";
public static final String FILE_CONTENT_LENGTH = "content_length";
public static final String FILE_CONTENT_TYPE = "content_type";
Expand Down Expand Up @@ -190,7 +191,8 @@ static public class ProviderTableMeta implements BaseColumns {
FILE_TAGS,
FILE_METADATA_GPS,
FILE_INTERNAL_TWO_WAY_SYNC_TIMESTAMP,
FILE_INTERNAL_TWO_WAY_SYNC_RESULT);
FILE_INTERNAL_TWO_WAY_SYNC_RESULT,
FILE_INDICATOR);
public static final String FILE_DEFAULT_SORT_ORDER = FILE_NAME + " collate nocase asc";

// Columns of ocshares table
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ import androidx.appcompat.widget.SearchView
import androidx.core.view.MenuItemCompat
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.dialog.MaterialAlertDialogBuilder
Expand All @@ -61,6 +63,8 @@ import com.nextcloud.client.core.Clock
import com.nextcloud.client.di.Injectable
import com.nextcloud.client.editimage.EditImageActivity
import com.nextcloud.client.files.DeepLinkHandler
import com.nextcloud.client.files.FileIndicator
import com.nextcloud.client.files.FileIndicatorManager
import com.nextcloud.client.jobs.download.FileDownloadHelper
import com.nextcloud.client.jobs.download.FileDownloadWorker
import com.nextcloud.client.jobs.download.FileDownloadWorker.Companion.getDownloadAddedMessage
Expand Down Expand Up @@ -157,6 +161,8 @@ import com.owncloud.android.utils.PushUtils
import com.owncloud.android.utils.StringUtils
import com.owncloud.android.utils.theme.CapabilityUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus
Expand Down Expand Up @@ -279,6 +285,7 @@ class FileDisplayActivity :
startMetadataSyncForRoot()
handleBackPress()
setupDrawer(menuItemId)
observeFileIndicatorState()
}

/**
Expand Down Expand Up @@ -1690,6 +1697,12 @@ class FileDisplayActivity :
sameFile = file?.remotePath == uploadedRemotePath || renamedInUpload
}

if (uploadWasFine) {
file?.let {
setIdleFileIndicator(it, includeSubFiles = false)
}
}

if (sameAccount && sameFile && this@FileDisplayActivity.leftFragment is FileDetailFragment) {
val fileDetailFragment = leftFragment as FileDetailFragment
if (uploadWasFine) {
Expand Down Expand Up @@ -2130,6 +2143,7 @@ class FileDisplayActivity :
}
supportInvalidateOptionsMenu()
fetchRecommendedFilesIfNeeded(ignoreETag = true, currentDir)
setIdleFileIndicator(removedFile)
} else {
if (result.isSslRecoverableException) {
mLastSslUntrustedServerResult = result
Expand All @@ -2138,6 +2152,36 @@ class FileDisplayActivity :
}
}

private fun setIdleFileIndicator(file: OCFile, includeSubFiles: Boolean = true) {
FileIndicatorManager.update(file.fileId, FileIndicator.Idle)

// while uploading files don't include so that downloaded icon can be removed for directory
if (!includeSubFiles) {
return
}

// while removing files include sub files since it's needed
lifecycleScope.launch(Dispatchers.IO) {
if (user.isEmpty) {
return@launch
}

if (file.isFolder) {
// clearing first depth child files
storageManager.fileDao.getSubfiles(file.fileId, user.get().accountName).forEach {
it.id?.let { id ->
FileIndicatorManager.update(id, FileIndicator.Idle)
}
}
} else {
val parent = storageManager.getFileById(file.parentId)
parent?.fileId?.let { parentId ->
FileIndicatorManager.update(parentId, FileIndicator.Idle)
}
}
}
}

private fun onRestoreFileVersionOperationFinish(result: RemoteOperationResult<*>) {
if (result.isSuccess) {
val file = getFile()
Expand Down Expand Up @@ -3073,6 +3117,34 @@ class FileDisplayActivity :
}
// endregion

@Suppress("MagicNumber")
@OptIn(FlowPreview::class)
private fun observeFileIndicatorState() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
FileIndicatorManager.activeTransfers
.debounce(100)
.collect { indicators ->
Log_OC.d(TAG, "observing file indicators")

withContext(Dispatchers.Main) {
// update UI with hot data
listOfFilesFragment?.adapter?.updateFileIndicators(indicators)

// update cold data in background
launch(Dispatchers.IO) {
storageManager.fileDao.updateFileIndicatorsBatch(
indicators.map { (fileId, indicator) ->
fileId to indicator.iconRes
}
)
}
}
}
}
}
}

companion object {
const val RESTART: String = "RESTART"
const val ALL_FILES: String = "ALL_FILES"
Expand Down
Loading
Loading