diff --git a/opencloudApp/src/main/java/eu/opencloud/android/utils/NotificationUtils.kt b/opencloudApp/src/main/java/eu/opencloud/android/utils/NotificationUtils.kt index 7b524f9112..6b15129cc6 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/utils/NotificationUtils.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/utils/NotificationUtils.kt @@ -179,12 +179,26 @@ object NotificationUtils { showConflictActivityIntent, pendingIntentFlags ) ) - var notificationId = 0 - // We need a notification id for each file in conflict, let's use the file id but in a safe way - if (fileInConflict.id!!.toInt() >= Int.MIN_VALUE && fileInConflict.id!!.toInt() <= Int.MAX_VALUE) { - notificationId = fileInConflict.id!!.toInt() - } + val notificationId = getConflictNotificationId(fileInConflict) notificationManager.notify(notificationId, notificationBuilder.build()) } + + internal fun getConflictNotificationId(fileInConflict: OCFile): Int { + val fileId = fileInConflict.id + return if (fileId != null && fileId in Int.MIN_VALUE.toLong()..Int.MAX_VALUE.toLong()) { + fileId.toInt() + } else { + stableConflictNotificationFallback(fileInConflict, fileId) + } + } + + private fun stableConflictNotificationFallback(fileInConflict: OCFile, fileId: Long?): Int = + listOf( + fileId?.toString().orEmpty(), + fileInConflict.owner, + fileInConflict.spaceId.orEmpty(), + fileInConflict.remoteId.orEmpty(), + fileInConflict.remotePath, + ).joinToString(separator = "|").hashCode() } diff --git a/opencloudApp/src/test/java/eu/opencloud/android/utils/NotificationUtilsTest.kt b/opencloudApp/src/test/java/eu/opencloud/android/utils/NotificationUtilsTest.kt new file mode 100644 index 0000000000..9c9a4ffd97 --- /dev/null +++ b/opencloudApp/src/test/java/eu/opencloud/android/utils/NotificationUtilsTest.kt @@ -0,0 +1,67 @@ +/** + * openCloud Android client application + * + * Copyright (C) 2026 openCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package eu.opencloud.android.utils + +import eu.opencloud.android.domain.files.model.OCFile +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Test + +class NotificationUtilsTest { + + @Test + fun conflictNotificationIdUsesIntRangeFileId() { + val file = conflictFile(id = 42L) + + assertEquals(42, NotificationUtils.getConflictNotificationId(file)) + } + + @Test + fun conflictNotificationIdUsesStableFallbackForLargeFileId() { + val file = conflictFile(id = Long.MAX_VALUE) + + val notificationId = NotificationUtils.getConflictNotificationId(file) + + assertNotEquals(Long.MAX_VALUE.toInt(), notificationId) + assertEquals(notificationId, NotificationUtils.getConflictNotificationId(file.copy())) + } + + @Test + fun conflictNotificationIdUsesStableFallbackForMissingFileId() { + val file = conflictFile(id = null) + + val notificationId = NotificationUtils.getConflictNotificationId(file) + + assertEquals(notificationId, NotificationUtils.getConflictNotificationId(file.copy())) + assertNotEquals( + notificationId, + NotificationUtils.getConflictNotificationId(file.copy(remotePath = "/Documents/other.txt")) + ) + } + + private fun conflictFile(id: Long?) = OCFile( + id = id, + owner = "user@example.org", + remotePath = "/Documents/conflict.txt", + remoteId = "remote-file-id", + length = 1L, + modificationTimestamp = 1L, + mimeType = "text/plain", + spaceId = "space-id", + ) +}