From 5c771a212fff5f2dd8fb571b31d8eaa0c7cdda79 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Mon, 12 Jan 2026 09:49:15 +0100 Subject: [PATCH 1/4] do not show all sync conflict notifications during app launch Signed-off-by: alperozturk # Conflicts: # app/src/main/res/values/strings.xml --- app/src/main/AndroidManifest.xml | 3 + .../client/jobs/upload/FileUploadHelper.kt | 12 +++ .../AppWideNotificationManager.kt | 79 +++++++++++++++++++ ...ncConflictNotificationBroadcastReceiver.kt | 33 ++++++++ 4 files changed, 127 insertions(+) create mode 100644 app/src/main/java/com/nextcloud/client/notifications/AppWideNotificationManager.kt create mode 100644 app/src/main/java/com/nextcloud/client/notifications/action/SyncConflictNotificationBroadcastReceiver.kt 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..929d2dc0331d --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/notifications/AppWideNotificationManager.kt @@ -0,0 +1,79 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.notifications + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +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.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 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.uploader_upload_failed_sync_conflict_error)) + .setContentText(context.getString(R.string.upload_conflict_message)) + .setStyle( + NotificationCompat.BigTextStyle() + .bigText(context.getString(R.string.upload_conflict_message)) + ) + .addAction( + R.drawable.ic_cloud_upload, + context.getString(R.string.upload_list_resolve_conflict), + actionPendingIntent + ) + .setContentIntent(pendingIntent) + .setAutoCancel(true) + .setOnlyAlertOnce(true) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .build() + + 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) + } +} From 0dfd4491de1fbf93b65eb622983a89cc31be12e8 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 14 Jan 2026 08:54:59 +0100 Subject: [PATCH 2/4] fix: lint Signed-off-by: alperozturk96 --- .../AppWideNotificationManager.kt | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/notifications/AppWideNotificationManager.kt b/app/src/main/java/com/nextcloud/client/notifications/AppWideNotificationManager.kt index 929d2dc0331d..5a1ad0a6ada0 100644 --- a/app/src/main/java/com/nextcloud/client/notifications/AppWideNotificationManager.kt +++ b/app/src/main/java/com/nextcloud/client/notifications/AppWideNotificationManager.kt @@ -7,13 +7,18 @@ 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.annotation.RequiresPermission +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 @@ -26,11 +31,14 @@ import com.owncloud.android.ui.notifications.NotificationUtils */ 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 @@ -56,15 +64,15 @@ object AppWideNotificationManager { val notification = NotificationCompat.Builder(context, NotificationUtils.NOTIFICATION_CHANNEL_UPLOAD) .setSmallIcon(R.drawable.uploads) - .setContentTitle(context.getString(R.string.uploader_upload_failed_sync_conflict_error)) - .setContentText(context.getString(R.string.upload_conflict_message)) + .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.upload_conflict_message)) + .bigText(context.getString(R.string.sync_conflict_notification_description)) ) .addAction( R.drawable.ic_cloud_upload, - context.getString(R.string.upload_list_resolve_conflict), + context.getString(R.string.sync_conflict_notification_action_title), actionPendingIntent ) .setContentIntent(pendingIntent) @@ -73,6 +81,15 @@ object AppWideNotificationManager { .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) } From c5cd446a0decdd3850b48c1bcd06e72234f6b51e Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 14 Jan 2026 08:55:28 +0100 Subject: [PATCH 3/4] fix: lint Signed-off-by: alperozturk96 [skip ci] --- .../client/notifications/AppWideNotificationManager.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/notifications/AppWideNotificationManager.kt b/app/src/main/java/com/nextcloud/client/notifications/AppWideNotificationManager.kt index 5a1ad0a6ada0..f58ad90ef0a4 100644 --- a/app/src/main/java/com/nextcloud/client/notifications/AppWideNotificationManager.kt +++ b/app/src/main/java/com/nextcloud/client/notifications/AppWideNotificationManager.kt @@ -12,7 +12,6 @@ import android.app.PendingIntent import android.content.Context import android.content.Intent import android.content.pm.PackageManager -import androidx.annotation.RequiresPermission import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat @@ -38,7 +37,6 @@ object AppWideNotificationManager { 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 From 3a94b9ef5cb917b312dc6851261e65c00a938cf3 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 14 Jan 2026 15:04:26 +0100 Subject: [PATCH 4/4] fix Signed-off-by: alperozturk96 --- app/src/main/res/values/strings.xml | 4 ++++ 1 file changed, 4 insertions(+) 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