diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt index 401fee165ac3..b7d06ee2d9d3 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt @@ -179,7 +179,8 @@ class BackgroundJobFactory @Inject constructor( powerManagementService = powerManagementService, syncedFolderProvider = syncedFolderProvider, backgroundJobManager = backgroundJobManager.get(), - repository = FileSystemRepository(dao = database.fileSystemDao()) + repository = FileSystemRepository(dao = database.fileSystemDao()), + viewThemeUtils = viewThemeUtils.get() ) private fun createOfflineSyncWork(context: Context, params: WorkerParameters): OfflineSyncWork = OfflineSyncWork( diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadNotificationManager.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadNotificationManager.kt new file mode 100644 index 000000000000..3b4532ecbfa4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadNotificationManager.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.jobs.autoUpload + +import android.content.Context +import com.nextcloud.client.jobs.notification.WorkerNotificationManager +import com.owncloud.android.R +import com.owncloud.android.ui.notifications.NotificationUtils +import com.owncloud.android.utils.theme.ViewThemeUtils + +class AutoUploadNotificationManager(context: Context, viewThemeUtils: ViewThemeUtils, id: Int) : + WorkerNotificationManager( + id, + context, + viewThemeUtils, + tickerId = R.string.foreground_service_upload, + channelId = NotificationUtils.NOTIFICATION_CHANNEL_UPLOAD + ) diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt index 2bef1d4bedd6..39856a809d1d 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt @@ -8,10 +8,8 @@ package com.nextcloud.client.jobs.autoUpload import android.app.Notification -import android.app.NotificationManager import android.content.Context import android.content.res.Resources -import androidx.core.app.NotificationCompat import androidx.exifinterface.media.ExifInterface import androidx.work.CoroutineWorker import androidx.work.WorkerParameters @@ -23,6 +21,7 @@ import com.nextcloud.client.database.entity.toUploadEntity import com.nextcloud.client.device.PowerManagementService import com.nextcloud.client.jobs.BackgroundJobManager import com.nextcloud.client.jobs.upload.FileUploadWorker +import com.nextcloud.client.jobs.utils.UploadErrorNotificationManager import com.nextcloud.client.network.ConnectivityService import com.nextcloud.client.preferences.SubFolderRule import com.nextcloud.utils.ForegroundServiceHelper @@ -41,10 +40,10 @@ import com.owncloud.android.lib.common.OwnCloudClientManagerFactory import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.operations.UploadFileOperation import com.owncloud.android.ui.activity.SettingsActivity -import com.owncloud.android.ui.notifications.NotificationUtils import com.owncloud.android.utils.FileStorageUtils import com.owncloud.android.utils.FilesSyncHelper import com.owncloud.android.utils.MimeType +import com.owncloud.android.utils.theme.ViewThemeUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.File @@ -63,7 +62,8 @@ class AutoUploadWorker( private val powerManagementService: PowerManagementService, private val syncedFolderProvider: SyncedFolderProvider, private val backgroundJobManager: BackgroundJobManager, - private val repository: FileSystemRepository + private val repository: FileSystemRepository, + val viewThemeUtils: ViewThemeUtils ) : CoroutineWorker(context, params) { companion object { @@ -71,16 +71,12 @@ class AutoUploadWorker( const val OVERRIDE_POWER_SAVING = "overridePowerSaving" const val CONTENT_URIS = "content_uris" const val SYNCED_FOLDER_ID = "syncedFolderId" - private const val CHANNEL_ID = NotificationUtils.NOTIFICATION_CHANNEL_UPLOAD - - private const val NOTIFICATION_ID = 266 + const val NOTIFICATION_ID = 266 } private val helper = AutoUploadHelper() private lateinit var syncedFolder: SyncedFolder - private val notificationManager by lazy { - context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - } + private val notificationManager = AutoUploadNotificationManager(context, viewThemeUtils, NOTIFICATION_ID) @Suppress("TooGenericExceptionCaught", "ReturnCount") override suspend fun doWork(): Result { @@ -124,7 +120,7 @@ class AutoUploadWorker( ) ) - notificationManager.notify(NOTIFICATION_ID, startNotification) + notificationManager.showNotification(startNotification) } } @@ -137,7 +133,7 @@ class AutoUploadWorker( setForeground(foregroundInfo) } - private fun createNotification(title: String): Notification = NotificationCompat.Builder(context, CHANNEL_ID) + private fun createNotification(title: String): Notification = notificationManager.notificationBuilder .setContentTitle(title) .setSmallIcon(R.drawable.uploads) .setOngoing(true) @@ -259,7 +255,7 @@ class AutoUploadWorker( SettingsActivity.SYNCED_FOLDER_LIGHT_UPLOAD_ON_WIFI ) val uploadActionString = context.resources.getString(R.string.syncedFolder_light_upload_behaviour) - val uploadAction = getUploadAction(uploadActionString) + val uploadAction = FileUploadWorker.getUploadAction(uploadActionString) Log_OC.d(TAG, "upload action is: $uploadAction") Triple(needsCharging, needsWifi, uploadAction) } else { @@ -314,6 +310,13 @@ class AutoUploadWorker( val result = operation.execute(client) uploadsStorageManager.updateStatus(uploadEntity, result.isSuccess) + UploadErrorNotificationManager.handleResult( + context, + notificationManager, + operation, + result + ) + if (result.isSuccess) { repository.markFileAsUploaded(localPath, syncedFolder) Log_OC.d(TAG, "✅ upload completed: $localPath") @@ -470,11 +473,4 @@ class AutoUploadWorker( } return lastModificationTime } - - private fun getUploadAction(action: String): Int = when (action) { - "LOCAL_BEHAVIOUR_FORGET" -> FileUploadWorker.Companion.LOCAL_BEHAVIOUR_FORGET - "LOCAL_BEHAVIOUR_MOVE" -> FileUploadWorker.Companion.LOCAL_BEHAVIOUR_MOVE - "LOCAL_BEHAVIOUR_DELETE" -> FileUploadWorker.Companion.LOCAL_BEHAVIOUR_DELETE - else -> FileUploadWorker.Companion.LOCAL_BEHAVIOUR_FORGET - } } diff --git a/app/src/main/java/com/nextcloud/client/jobs/notification/WorkerNotificationManager.kt b/app/src/main/java/com/nextcloud/client/jobs/notification/WorkerNotificationManager.kt index e427142decda..573e1d1d02ac 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/notification/WorkerNotificationManager.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/notification/WorkerNotificationManager.kt @@ -46,6 +46,14 @@ open class WorkerNotificationManager( notificationManager.notify(id, notificationBuilder.build()) } + fun showNotification(notification: Notification) { + notificationManager.notify(id, notification) + } + + fun showNotification(id: Int, notification: Notification) { + notificationManager.notify(id, notification) + } + @Suppress("MagicNumber") fun setProgress(percent: Int, progressText: String?, indeterminate: Boolean) { notificationBuilder.run { diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadBroadcastReceiver.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadBroadcastReceiver.kt index eca87bfd3751..7f7df3a32cb3 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadBroadcastReceiver.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadBroadcastReceiver.kt @@ -8,12 +8,12 @@ package com.nextcloud.client.jobs.upload import android.app.NotificationManager +import android.app.PendingIntent import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import com.owncloud.android.MainApp import com.owncloud.android.datamodel.UploadsStorageManager -import com.owncloud.android.ui.notifications.NotificationUtils import javax.inject.Inject class FileUploadBroadcastReceiver : BroadcastReceiver() { @@ -22,17 +22,28 @@ class FileUploadBroadcastReceiver : BroadcastReceiver() { lateinit var uploadsStorageManager: UploadsStorageManager companion object { - const val UPLOAD_ID = "UPLOAD_ID" - const val REMOTE_PATH = "REMOTE_PATH" - const val STORAGE_PATH = "STORAGE_PATH" + private const val UPLOAD_ID = "UPLOAD_ID" + + fun getBroadcast(context: Context, id: Long): PendingIntent { + val intent = Intent(context, FileUploadBroadcastReceiver::class.java).apply { + putExtra(UPLOAD_ID, id) + setClass(context, FileUploadBroadcastReceiver::class.java) + setPackage(context.packageName) + } + + return PendingIntent.getBroadcast( + context, + id.toInt(), + intent, + PendingIntent.FLAG_IMMUTABLE + ) + } } @Suppress("ReturnCount") override fun onReceive(context: Context, intent: Intent) { MainApp.getAppComponent().inject(this) - val remotePath = intent.getStringExtra(REMOTE_PATH) ?: return - val storagePath = intent.getStringExtra(STORAGE_PATH) ?: return val uploadId = intent.getLongExtra(UPLOAD_ID, -1L) if (uploadId == -1L) { return @@ -40,9 +51,6 @@ class FileUploadBroadcastReceiver : BroadcastReceiver() { uploadsStorageManager.removeUpload(uploadId) val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - notificationManager.cancel( - NotificationUtils.createUploadNotificationTag(remotePath, storagePath), - FileUploadWorker.NOTIFICATION_ERROR_ID - ) + notificationManager.cancel(uploadId.toInt()) } } 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 38cc85bc283e..c29f9c6a63d9 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 @@ -8,7 +8,6 @@ package com.nextcloud.client.jobs.upload import android.app.Notification -import android.app.PendingIntent import android.content.Context import androidx.core.app.NotificationCompat import androidx.localbroadcastmanager.content.LocalBroadcastManager @@ -19,6 +18,7 @@ import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.device.PowerManagementService import com.nextcloud.client.jobs.BackgroundJobManager import com.nextcloud.client.jobs.BackgroundJobManagerImpl +import com.nextcloud.client.jobs.utils.UploadErrorNotificationManager import com.nextcloud.client.network.ConnectivityService import com.nextcloud.client.preferences.AppPreferences import com.nextcloud.model.WorkerState @@ -41,7 +41,6 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCo import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.operations.UploadFileOperation import com.owncloud.android.ui.notifications.NotificationUtils -import com.owncloud.android.utils.ErrorMessageAdapter import com.owncloud.android.utils.theme.ViewThemeUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ensureActive @@ -118,6 +117,13 @@ class FileUploadWorker( return false } + + fun getUploadAction(action: String): Int = when (action) { + "LOCAL_BEHAVIOUR_FORGET" -> LOCAL_BEHAVIOUR_FORGET + "LOCAL_BEHAVIOUR_MOVE" -> LOCAL_BEHAVIOUR_MOVE + "LOCAL_BEHAVIOUR_DELETE" -> LOCAL_BEHAVIOUR_DELETE + else -> LOCAL_BEHAVIOUR_FORGET + } } private var lastPercent = 0 @@ -265,7 +271,7 @@ class FileUploadWorker( if (result.code == ResultCode.QUOTA_EXCEEDED) { Log_OC.w(TAG, "Quota exceeded, stopping uploads") - notificationManager.showQuotaExceedNotification(operation, result.code) + notificationManager.showQuotaExceedNotification(operation) break } @@ -329,125 +335,45 @@ class FileUploadWorker( } @Suppress("TooGenericExceptionCaught", "DEPRECATION") - private fun upload( - uploadFileOperation: UploadFileOperation, + private suspend fun upload( + operation: UploadFileOperation, user: User, client: OwnCloudClient - ): RemoteOperationResult { + ): RemoteOperationResult = withContext(Dispatchers.IO) { lateinit var result: RemoteOperationResult try { - val storageManager = uploadFileOperation.storageManager - result = uploadFileOperation.execute(client) + val storageManager = operation.storageManager + result = operation.execute(client) val task = ThumbnailsCacheManager.ThumbnailGenerationTask(storageManager, user) - val file = File(uploadFileOperation.originalStoragePath) - val remoteId: String? = uploadFileOperation.file.remoteId + val file = File(operation.originalStoragePath) + val remoteId: String? = operation.file.remoteId task.execute(ThumbnailsCacheManager.ThumbnailGenerationTaskObject(file, remoteId)) } catch (e: Exception) { Log_OC.e(TAG, "Error uploading", e) result = RemoteOperationResult(e) } finally { - cleanupUploadProcess(result, uploadFileOperation) - } - - return result - } - - private fun cleanupUploadProcess(result: RemoteOperationResult, uploadFileOperation: UploadFileOperation) { - if (!isStopped || !result.isCancelled) { - uploadsStorageManager.updateDatabaseUploadResult(result, uploadFileOperation) - notifyUploadResult(uploadFileOperation, result) - } - } - - @Suppress("ReturnCount", "LongMethod") - private fun notifyUploadResult( - uploadFileOperation: UploadFileOperation, - uploadResult: RemoteOperationResult - ) { - Log_OC.d(TAG, "NotifyUploadResult with resultCode: " + uploadResult.code) - val showSameFileAlreadyExistsNotification = - inputData.getBoolean(SHOW_SAME_FILE_ALREADY_EXISTS_NOTIFICATION, false) - - if (uploadResult.isSuccess) { - notificationManager.dismissOldErrorNotification(uploadFileOperation) - return - } - - if (uploadResult.isCancelled) { - return - } - - // Only notify if it is not same file on remote that causes conflict - if (uploadResult.code == ResultCode.SYNC_CONFLICT && - FileUploadHelper().isSameFileOnRemote( - uploadFileOperation.user, - File(uploadFileOperation.storagePath), - uploadFileOperation.remotePath, - context - ) - ) { - if (showSameFileAlreadyExistsNotification) { - notificationManager.showSameFileAlreadyExistsNotification(uploadFileOperation.fileName) + if (!isStopped || !result.isCancelled) { + uploadsStorageManager.updateDatabaseUploadResult(result, operation) + UploadErrorNotificationManager.handleResult( + context, + notificationManager, + operation, + result, + showSameFileAlreadyExistsNotification = { + withContext(Dispatchers.Main) { + val showSameFileAlreadyExistsNotification = + inputData.getBoolean(SHOW_SAME_FILE_ALREADY_EXISTS_NOTIFICATION, false) + if (showSameFileAlreadyExistsNotification) { + notificationManager.showSameFileAlreadyExistsNotification(operation.fileName) + } + } + } + ) } - - uploadFileOperation.handleLocalBehaviour() - return - } - - val notDelayed = uploadResult.code !in setOf( - ResultCode.DELAYED_FOR_WIFI, - ResultCode.DELAYED_FOR_CHARGING, - ResultCode.DELAYED_IN_POWER_SAVE_MODE - ) - - val isValidFile = uploadResult.code !in setOf( - ResultCode.LOCAL_FILE_NOT_FOUND, - ResultCode.LOCK_FAILED - ) - - if (!notDelayed || !isValidFile) { - return } - if (uploadResult.code == ResultCode.USER_CANCELLED) { - return - } - - notificationManager.run { - val errorMessage = ErrorMessageAdapter.getErrorCauseMessage( - uploadResult, - uploadFileOperation, - context.resources - ) - - val conflictResolveIntent = if (uploadResult.code == ResultCode.SYNC_CONFLICT) { - intents.conflictResolveActionIntents(context, uploadFileOperation) - } else { - null - } - - val credentialIntent: PendingIntent? = if (uploadResult.code == ResultCode.UNAUTHORIZED) { - intents.credentialIntent(uploadFileOperation) - } else { - null - } - - val cancelUploadActionIntent = if (conflictResolveIntent != null) { - intents.cancelUploadActionIntent(uploadFileOperation) - } else { - null - } - - notifyForFailedResult( - uploadFileOperation, - uploadResult.code, - conflictResolveIntent, - cancelUploadActionIntent, - credentialIntent, - errorMessage - ) - } + return@withContext result } @Suppress("MagicNumber") diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploaderIntents.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploaderIntents.kt index b99310fa1e09..ad0101aa5d59 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploaderIntents.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploaderIntents.kt @@ -10,10 +10,7 @@ package com.nextcloud.client.jobs.upload import android.app.PendingIntent import android.content.Context import android.content.Intent -import android.os.Build -import com.owncloud.android.authentication.AuthenticatorActivity import com.owncloud.android.operations.UploadFileOperation -import com.owncloud.android.ui.activity.ConflictsResolveActivity.Companion.createIntent import com.owncloud.android.ui.activity.UploadListActivity import java.security.SecureRandom @@ -39,23 +36,6 @@ class FileUploaderIntents(private val context: Context) { ) } - fun credentialIntent(operation: UploadFileOperation): PendingIntent { - val intent = Intent(context, AuthenticatorActivity::class.java).apply { - putExtra(AuthenticatorActivity.EXTRA_ACCOUNT, operation.user.toPlatformAccount()) - putExtra(AuthenticatorActivity.EXTRA_ACTION, AuthenticatorActivity.ACTION_UPDATE_EXPIRED_TOKEN) - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) - addFlags(Intent.FLAG_FROM_BACKGROUND) - } - - return PendingIntent.getActivity( - context, - System.currentTimeMillis().toInt(), - intent, - PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE - ) - } - fun notificationStartIntent(operation: UploadFileOperation?): PendingIntent { val intent = UploadListActivity.createIntent( operation?.file, @@ -71,40 +51,4 @@ class FileUploaderIntents(private val context: Context) { PendingIntent.FLAG_IMMUTABLE ) } - - fun conflictResolveActionIntents(context: Context, uploadFileOperation: UploadFileOperation): PendingIntent { - val intent = createIntent( - uploadFileOperation.file, - uploadFileOperation.user, - uploadFileOperation.ocUploadId, - Intent.FLAG_ACTIVITY_CLEAR_TOP, - context - ) - - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - PendingIntent.getActivity(context, SecureRandom().nextInt(), intent, PendingIntent.FLAG_MUTABLE) - } else { - PendingIntent.getActivity( - context, - SecureRandom().nextInt(), - intent, - PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE - ) - } - } - - fun cancelUploadActionIntent(uploadFileOperation: UploadFileOperation): PendingIntent { - val intent = Intent(context, FileUploadBroadcastReceiver::class.java).apply { - putExtra(FileUploadBroadcastReceiver.UPLOAD_ID, uploadFileOperation.ocUploadId) - putExtra(FileUploadBroadcastReceiver.REMOTE_PATH, uploadFileOperation.file.remotePath) - putExtra(FileUploadBroadcastReceiver.STORAGE_PATH, uploadFileOperation.file.storagePath) - } - - return PendingIntent.getBroadcast( - context, - 0, - intent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - } } diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/UploadNotificationManager.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/UploadNotificationManager.kt index 9aae34575ac2..b555b6fab83c 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/UploadNotificationManager.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/UploadNotificationManager.kt @@ -10,11 +10,8 @@ package com.nextcloud.client.jobs.upload import android.app.PendingIntent import android.content.Context import com.nextcloud.client.jobs.notification.WorkerNotificationManager -import com.nextcloud.utils.extensions.isFileSpecificError import com.nextcloud.utils.numberFormatter.NumberFormatter import com.owncloud.android.R -import com.owncloud.android.lib.common.operations.RemoteOperationResult -import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode import com.owncloud.android.operations.UploadFileOperation import com.owncloud.android.ui.notifications.NotificationUtils import com.owncloud.android.utils.theme.ViewThemeUtils @@ -78,86 +75,6 @@ class UploadNotificationManager(private val context: Context, viewThemeUtils: Vi dismissOldErrorNotification(currentOperation) } - fun notifyForFailedResult( - uploadFileOperation: UploadFileOperation, - resultCode: RemoteOperationResult.ResultCode, - conflictsResolveIntent: PendingIntent?, - cancelUploadActionIntent: PendingIntent?, - credentialIntent: PendingIntent?, - errorMessage: String - ) { - if (uploadFileOperation.isMissingPermissionThrown) { - return - } - - val textId = getFailedResultTitleId(resultCode) - - notificationBuilder.run { - setTicker(context.getString(textId)) - setContentTitle(context.getString(textId)) - setAutoCancel(false) - setOngoing(false) - setProgress(0, 0, false) - clearActions() - - conflictsResolveIntent?.let { - addAction( - R.drawable.ic_cloud_upload, - R.string.upload_list_resolve_conflict, - it - ) - } - - cancelUploadActionIntent?.let { - addAction( - R.drawable.ic_delete, - R.string.upload_list_cancel_upload, - cancelUploadActionIntent - ) - } - - credentialIntent?.let { - setContentIntent(it) - } - - setContentText(errorMessage) - } - - if (resultCode.isFileSpecificError()) { - showNewNotification(uploadFileOperation) - } else { - showNotification() - } - } - - private fun getFailedResultTitleId(resultCode: RemoteOperationResult.ResultCode): Int { - val needsToUpdateCredentials = (resultCode == RemoteOperationResult.ResultCode.UNAUTHORIZED) - - return if (needsToUpdateCredentials) { - R.string.uploader_upload_failed_credentials_error - } else if (resultCode == RemoteOperationResult.ResultCode.SYNC_CONFLICT) { - R.string.uploader_upload_failed_sync_conflict_error - } else { - R.string.uploader_upload_failed_ticker - } - } - - fun addAction(icon: Int, textId: Int, intent: PendingIntent) { - notificationBuilder.addAction( - icon, - context.getString(textId), - intent - ) - } - - private fun showNewNotification(operation: UploadFileOperation) { - notificationManager.notify( - NotificationUtils.createUploadNotificationTag(operation.file), - operation.file.hashCode(), - notificationBuilder.build() - ) - } - fun showSameFileAlreadyExistsNotification(filename: String) { notificationBuilder.run { setAutoCancel(true) @@ -175,15 +92,15 @@ class UploadNotificationManager(private val context: Context, viewThemeUtils: Vi ) } - fun showQuotaExceedNotification(operation: UploadFileOperation, resultCode: ResultCode) { - notifyForFailedResult( - operation, - resultCode, - null, - null, - null, - context.getString(R.string.upload_quota_exceeded) - ) + fun showQuotaExceedNotification(operation: UploadFileOperation) { + val notification = notificationBuilder.run { + setContentTitle(context.getString(R.string.upload_quota_exceeded)) + setContentText("") + clearActions() + setProgress(0, 0, false) + }.build() + + showNotification(operation.file.fileId.toInt(), notification) } fun showConnectionErrorNotification() { diff --git a/app/src/main/java/com/nextcloud/client/jobs/utils/UploadErrorNotificationManager.kt b/app/src/main/java/com/nextcloud/client/jobs/utils/UploadErrorNotificationManager.kt new file mode 100644 index 000000000000..0ae1ec154d05 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/jobs/utils/UploadErrorNotificationManager.kt @@ -0,0 +1,191 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.jobs.utils + +import android.app.Notification +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import androidx.core.app.NotificationCompat +import com.nextcloud.client.jobs.notification.WorkerNotificationManager +import com.nextcloud.client.jobs.upload.FileUploadBroadcastReceiver +import com.nextcloud.client.jobs.upload.FileUploadHelper +import com.nextcloud.utils.extensions.isFileSpecificError +import com.owncloud.android.R +import com.owncloud.android.authentication.AuthenticatorActivity +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.operations.UploadFileOperation +import com.owncloud.android.ui.activity.ConflictsResolveActivity +import com.owncloud.android.utils.ErrorMessageAdapter +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File + +object UploadErrorNotificationManager { + private const val TAG = "UploadErrorNotificationManager" + + suspend fun handleResult( + context: Context, + notificationManager: WorkerNotificationManager, + operation: UploadFileOperation, + result: RemoteOperationResult, + showSameFileAlreadyExistsNotification: suspend () -> Unit = {} + ) { + Log_OC.d(TAG, "handle upload result with result code: " + result.code) + + val notification = withContext(Dispatchers.IO) { + val isSameFileOnRemote = FileUploadHelper.instance().isSameFileOnRemote( + operation.user, + File(operation.storagePath), + operation.remotePath, + context + ) + + getNotification( + isSameFileOnRemote, + context, + notificationManager.notificationBuilder, + operation, + result, + notifyOnSameFileExists = { + showSameFileAlreadyExistsNotification() + operation.handleLocalBehaviour() + } + ) + } ?: return + + Log_OC.d(TAG, "🔔" + "notification created") + + withContext(Dispatchers.Main) { + if (result.code.isFileSpecificError()) { + notificationManager.showNotification(operation.ocUploadId.toInt(), notification) + } else { + notificationManager.showNotification(notification) + } + } + } + + private suspend fun getNotification( + isSameFileOnRemote: Boolean, + context: Context, + builder: NotificationCompat.Builder, + operation: UploadFileOperation, + result: RemoteOperationResult, + notifyOnSameFileExists: suspend () -> Unit + ): Notification? { + if (!shouldShowConflictDialog(isSameFileOnRemote, operation, result, notifyOnSameFileExists)) return null + + val textId = result.code.toFailedResultTitleId() + val errorMessage = ErrorMessageAdapter.getErrorCauseMessage(result, operation, context.resources) + + return builder.apply { + setTicker(context.getString(textId)) + setContentTitle(context.getString(textId)) + setContentText(errorMessage) + setAutoCancel(false) + setOngoing(false) + setProgress(0, 0, false) + clearActions() + + result.code.takeIf { it == ResultCode.SYNC_CONFLICT }?.let { + addAction( + R.drawable.ic_cloud_upload, + context.getString(R.string.upload_list_resolve_conflict), + conflictResolvePendingIntent(context, operation) + ) + addAction( + R.drawable.ic_delete, + context.getString(R.string.upload_list_cancel_upload), + FileUploadBroadcastReceiver.getBroadcast(context, operation.ocUploadId) + ) + } + + result.code.takeIf { it == ResultCode.UNAUTHORIZED }?.let { + setContentIntent(credentialPendingIntent(context, operation)) + } + }.build() + } + + private fun ResultCode.toFailedResultTitleId(): Int = when (this) { + ResultCode.UNAUTHORIZED -> R.string.uploader_upload_failed_credentials_error + ResultCode.SYNC_CONFLICT -> R.string.uploader_upload_failed_sync_conflict_error + else -> R.string.uploader_upload_failed_ticker + } + + private fun credentialPendingIntent(context: Context, operation: UploadFileOperation): PendingIntent { + val intent = Intent(context, AuthenticatorActivity::class.java).apply { + putExtra(AuthenticatorActivity.EXTRA_ACCOUNT, operation.user.toPlatformAccount()) + putExtra(AuthenticatorActivity.EXTRA_ACTION, AuthenticatorActivity.ACTION_UPDATE_EXPIRED_TOKEN) + addFlags( + Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS or + Intent.FLAG_FROM_BACKGROUND + ) + setClass(context, AuthenticatorActivity::class.java) + setPackage(context.packageName) + } + + return PendingIntent.getActivity( + context, + System.currentTimeMillis().toInt(), + intent, + PendingIntent.FLAG_IMMUTABLE + ) + } + + private fun conflictResolvePendingIntent(context: Context, operation: UploadFileOperation): PendingIntent { + val intent = ConflictsResolveActivity.createIntent( + operation.file, + operation.user, + conflictUploadId = operation.ocUploadId, + Intent.FLAG_ACTIVITY_CLEAR_TOP, + context + ).apply { + setClass(context, ConflictsResolveActivity::class.java) + setPackage(context.packageName) + } + + return PendingIntent.getActivity( + context, + operation.ocUploadId.toInt(), + intent, + PendingIntent.FLAG_IMMUTABLE + ) + } + + @Suppress("ReturnCount", "ComplexCondition") + private suspend fun shouldShowConflictDialog( + isSameFileOnRemote: Boolean, + operation: UploadFileOperation, + result: RemoteOperationResult, + notifyOnSameFileExists: suspend () -> Unit + ): Boolean { + if (result.isSuccess || + result.isCancelled || + result.code == ResultCode.USER_CANCELLED || + operation.isMissingPermissionThrown + ) { + Log_OC.w(TAG, "operation is successful, cancelled or lack of storage permission") + return false + } + + if (result.code == ResultCode.SYNC_CONFLICT && isSameFileOnRemote) { + Log_OC.w(TAG, "same file exists on remote") + notifyOnSameFileExists() + return false + } + + val delayedCodes = + setOf(ResultCode.DELAYED_FOR_WIFI, ResultCode.DELAYED_FOR_CHARGING, ResultCode.DELAYED_IN_POWER_SAVE_MODE) + val invalidCodes = setOf(ResultCode.LOCAL_FILE_NOT_FOUND, ResultCode.LOCK_FAILED) + + return result.code !in delayedCodes && result.code !in invalidCodes + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.kt index fc5f5799482e..c1ba17a82aec 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.kt @@ -41,7 +41,6 @@ import com.owncloud.android.lib.resources.files.model.RemoteFile import com.owncloud.android.ui.dialog.ConflictsResolveDialog import com.owncloud.android.ui.dialog.ConflictsResolveDialog.Decision import com.owncloud.android.ui.dialog.ConflictsResolveDialog.OnConflictDecisionMadeListener -import com.owncloud.android.ui.notifications.NotificationUtils import com.owncloud.android.utils.FileStorageUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -131,7 +130,7 @@ class ConflictsResolveActivity : updateThumbnailIfNeeded(decision, file, oldFile) } - dismissConflictResolveNotification(file) + dismissConflictResolveNotification() finish() } } @@ -149,12 +148,9 @@ class ConflictsResolveActivity : } } - private fun dismissConflictResolveNotification(file: OCFile?) { - file ?: return - + private fun dismissConflictResolveNotification() { val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager - val tag = NotificationUtils.createUploadNotificationTag(file) - notificationManager.cancel(tag, FileUploadWorker.NOTIFICATION_ERROR_ID) + notificationManager.cancel(conflictUploadId.toInt()) } private fun keepBothFolder(offlineOperation: OfflineOperationEntity?, serverFile: OCFile?) { 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 2ba620f78b16..be61e32398d8 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 @@ -894,7 +894,7 @@ class FileDisplayActivity : searchView?.postDelayed({ searchView?.isIconified = false searchView?.requestFocusFromTouch() - }, 100) + }, SEARCH_VIEW_FOCUS_DELAY) } searchView?.let { viewThemeUtils.androidx.themeToolbarSearchView(it) } @@ -3052,6 +3052,7 @@ class FileDisplayActivity : private const val DIALOG_TAG_SHOW_TOS = "DIALOG_TAG_SHOW_TOS" private const val ON_RESUMED_RESET_DELAY = 10000L + private const val SEARCH_VIEW_FOCUS_DELAY = 100L const val ACTION_DETAILS: String = "com.owncloud.android.ui.activity.action.DETAILS" diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/GalleryAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/GalleryAdapter.kt index 1b8d3cc96571..0c84fe6c84e3 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/GalleryAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/GalleryAdapter.kt @@ -43,7 +43,7 @@ import me.zhanghai.android.fastscroll.PopupTextProvider import java.util.Calendar import java.util.Date -@Suppress("LongParameterList") +@Suppress("LongParameterList", "TooManyFunctions") class GalleryAdapter( val context: Context, user: User,