diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 940739adb944..6a210d519cb1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -288,6 +288,9 @@ android:exported="false" tools:replace="android:exported" /> + diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt index 566d80415c08..0604e5db75f2 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt @@ -19,6 +19,7 @@ import com.nextcloud.client.device.BatteryStatus import com.nextcloud.client.device.PowerManagementService import com.nextcloud.client.jobs.BackgroundJobManager import com.nextcloud.client.jobs.upload.FileUploadWorker.Companion.currentUploadFileOperation +import com.nextcloud.client.notifications.AppWideNotificationManager import com.nextcloud.client.network.Connectivity import com.nextcloud.client.network.ConnectivityService import com.nextcloud.utils.extensions.getUploadIds @@ -170,6 +171,7 @@ class FileUploadHelper { uploads: Array ): Boolean { var showNotExistMessage = false + var showSyncConflictNotification = false val isOnline = checkConnectivity(connectivityService) val connectivity = connectivityService.connectivity val batteryStatus = powerManagementService.battery @@ -177,6 +179,12 @@ class FileUploadHelper { val uploadsToRetry = mutableListOf() for (upload in uploads) { + if (upload.lastResult == UploadResult.SYNC_CONFLICT) { + Log_OC.d(TAG, "retry upload skipped, sync conflict: ${upload.remotePath}") + showSyncConflictNotification = true + continue + } + val uploadResult = checkUploadConditions( upload, connectivity, @@ -214,6 +222,10 @@ class FileUploadHelper { ) } + if (showSyncConflictNotification) { + AppWideNotificationManager.showSyncConflictNotification(MainApp.getAppContext()) + } + return showNotExistMessage } diff --git a/app/src/main/java/com/nextcloud/client/notifications/AppWideNotificationManager.kt b/app/src/main/java/com/nextcloud/client/notifications/AppWideNotificationManager.kt new file mode 100644 index 000000000000..f58ad90ef0a4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/notifications/AppWideNotificationManager.kt @@ -0,0 +1,94 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.notifications + +import android.Manifest +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import androidx.core.app.ActivityCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import com.nextcloud.client.notifications.action.SyncConflictNotificationBroadcastReceiver +import com.owncloud.android.R +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.ui.activity.UploadListActivity +import com.owncloud.android.ui.notifications.NotificationUtils + +/** + * Responsible for showing **app-wide notifications** in the app. + * + * This manager provides a centralized place to create and display notifications + * that are not tied to a specific screen or feature. + * + */ +object AppWideNotificationManager { + + private const val TAG = "AppWideNotificationManager" + + private const val SYNC_CONFLICT_NOTIFICATION_INTENT_REQ_CODE = 16 + private const val SYNC_CONFLICT_NOTIFICATION_INTENT_ACTION_REQ_CODE = 17 + + private const val SYNC_CONFLICT_NOTIFICATION_ID = 112 + + fun showSyncConflictNotification(context: Context) { + val intent = Intent(context, UploadListActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + } + + val pendingIntent = PendingIntent.getActivity( + context, + SYNC_CONFLICT_NOTIFICATION_INTENT_REQ_CODE, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + val actionIntent = Intent(context, SyncConflictNotificationBroadcastReceiver::class.java).apply { + putExtra(SyncConflictNotificationBroadcastReceiver.NOTIFICATION_ID, SYNC_CONFLICT_NOTIFICATION_ID) + } + + val actionPendingIntent = PendingIntent.getBroadcast( + context, + SYNC_CONFLICT_NOTIFICATION_INTENT_ACTION_REQ_CODE, + actionIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + val notification = NotificationCompat.Builder(context, NotificationUtils.NOTIFICATION_CHANNEL_UPLOAD) + .setSmallIcon(R.drawable.uploads) + .setContentTitle(context.getString(R.string.sync_conflict_notification_title)) + .setContentText(context.getString(R.string.sync_conflict_notification_description)) + .setStyle( + NotificationCompat.BigTextStyle() + .bigText(context.getString(R.string.sync_conflict_notification_description)) + ) + .addAction( + R.drawable.ic_cloud_upload, + context.getString(R.string.sync_conflict_notification_action_title), + actionPendingIntent + ) + .setContentIntent(pendingIntent) + .setAutoCancel(true) + .setOnlyAlertOnce(true) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .build() + + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS + ) != PackageManager.PERMISSION_GRANTED + ) { + Log_OC.w(TAG, "cannot show sync conflict notification, post notification permission is not granted") + return + } + + NotificationManagerCompat.from(context) + .notify(SYNC_CONFLICT_NOTIFICATION_ID, notification) + } +} diff --git a/app/src/main/java/com/nextcloud/client/notifications/action/SyncConflictNotificationBroadcastReceiver.kt b/app/src/main/java/com/nextcloud/client/notifications/action/SyncConflictNotificationBroadcastReceiver.kt new file mode 100644 index 000000000000..db067a497b9e --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/notifications/action/SyncConflictNotificationBroadcastReceiver.kt @@ -0,0 +1,33 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.notifications.action + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import androidx.core.app.NotificationManagerCompat +import com.owncloud.android.ui.activity.UploadListActivity + +class SyncConflictNotificationBroadcastReceiver : BroadcastReceiver() { + companion object { + const val NOTIFICATION_ID = "NOTIFICATION_ID" + } + + override fun onReceive(context: Context, intent: Intent) { + val notificationId = intent.getIntExtra(NOTIFICATION_ID, -1) + + if (notificationId != -1) { + NotificationManagerCompat.from(context).cancel(notificationId) + } + + val intent = Intent(context, UploadListActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + } + context.startActivity(intent) + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9df94fe5e626..01cec706c75f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1475,4 +1475,8 @@ Failed to create conflict dialog Cannot open file chooser + + File upload conflicts + Upload conflicts detected. Open uploads to resolve. + Resolve conflicts