Skip to content
Merged
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
3 changes: 3 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,9 @@
android:exported="false"
tools:replace="android:exported" />

<receiver
android:name="com.nextcloud.client.notifications.action.SyncConflictNotificationBroadcastReceiver"
android:exported="false" />
<receiver
android:name="com.nextcloud.client.jobs.offlineOperations.receiver.OfflineOperationReceiver"
android:exported="false" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -170,13 +171,20 @@ class FileUploadHelper {
uploads: Array<OCUpload>
): Boolean {
var showNotExistMessage = false
var showSyncConflictNotification = false
val isOnline = checkConnectivity(connectivityService)
val connectivity = connectivityService.connectivity
val batteryStatus = powerManagementService.battery

val uploadsToRetry = mutableListOf<Long>()

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,
Expand Down Expand Up @@ -214,6 +222,10 @@ class FileUploadHelper {
)
}

if (showSyncConflictNotification) {
AppWideNotificationManager.showSyncConflictNotification(MainApp.getAppContext())
}

return showNotExistMessage
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <[email protected]>
* 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)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <[email protected]>
* 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)
}
}
4 changes: 4 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1476,4 +1476,8 @@
<string name="editor_web_view_cannot_open_file">Cannot open file chooser</string>
<string name="failed_to_start_action">Failed to start action!</string>
<string name="action_triggered">Action triggered</string>

<string name="sync_conflict_notification_title">File upload conflicts</string>
<string name="sync_conflict_notification_description">Upload conflicts detected. Open uploads to resolve.</string>
<string name="sync_conflict_notification_action_title">Resolve conflicts</string>
</resources>
Loading