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