diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddressBookStore.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddressBookStore.kt index 6fbb821e9..6b448ebb8 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddressBookStore.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddressBookStore.kt @@ -22,6 +22,8 @@ import at.bitfire.davdroid.settings.SettingsManager import at.bitfire.davdroid.sync.account.SystemAccountUtils import at.bitfire.davdroid.sync.account.setAndVerifyUserData import at.bitfire.davdroid.util.DavUtils.lastSegment +import at.bitfire.davdroid.util.MARKER +import at.bitfire.davdroid.util.encodeNumber import com.google.common.base.CharMatcher import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.channels.awaitClose @@ -74,7 +76,7 @@ class LocalAddressBookStore @Inject constructor( sb.append(" (${service.accountName})") } // Add the collection ID for uniqueness - sb.append(" #${info.id}") + sb.append(MARKER + encodeNumber(info.id)) return sb.toString() } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/account/SystemAccountUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/account/SystemAccountUtils.kt index 41259c232..80bd5b56e 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/account/SystemAccountUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/account/SystemAccountUtils.kt @@ -8,6 +8,8 @@ import android.accounts.Account import android.accounts.AccountManager import android.content.Context import android.os.Bundle +import at.bitfire.davdroid.util.MARKER +import at.bitfire.davdroid.util.decodeWithMarker import java.util.logging.Logger object SystemAccountUtils { @@ -67,4 +69,9 @@ fun AccountManager.setAndVerifyUserData(account: Account, key: String, value: St Thread.sleep(100) } Logger.getGlobal().warning("AccountManager failed to set $account user data $key := $value") -} \ No newline at end of file +} + +fun Account.nameWithNumber(): String { + val number = decodeWithMarker(name) ?: return name + return name.substringBeforeLast(MARKER) + " (#" + number + ")" +} diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/adapter/SyncFrameworkIntegration.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/adapter/SyncFrameworkIntegration.kt index 0ba61e15e..a6b8a12ab 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/adapter/SyncFrameworkIntegration.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/adapter/SyncFrameworkIntegration.kt @@ -12,6 +12,9 @@ import android.os.Bundle import androidx.annotation.WorkerThread import at.bitfire.davdroid.resource.LocalAddressBookStore import at.bitfire.davdroid.sync.SyncDataType +import at.bitfire.davdroid.sync.account.nameWithNumber +import at.bitfire.davdroid.util.MARKER +import at.bitfire.davdroid.util.decodeWithMarker import dagger.Lazy import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -256,7 +259,7 @@ class SyncFrameworkIntegration @Inject constructor( private fun anyPendingSync(accounts: List, authority: String): Boolean = accounts.any { account -> ContentResolver.isSyncPending(account, authority).also { pending -> - logger.finer("Sync pending($account, $authority) = $pending") + logger.finer("Sync pending(name=${account.nameWithNumber()}, type=${account.type}, $authority) = $pending") } } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/util/StringUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/util/StringUtils.kt index a4e76ddf0..fbc2febef 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/util/StringUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/util/StringUtils.kt @@ -13,4 +13,40 @@ fun String.withTrailingSlash() = if (this.endsWith('/')) this else - "$this/" \ No newline at end of file + "$this/" + +const val MARKER = '\uFEFF' // Start of hidden payload +const val ZERO = '\u200B' // Zero Width Space for 0 +const val ONE = '\u200C' // Zero Width Non-Joiner for 1 + +/** + * Encodes a number using our own steganographic encoding scheme to hide a number in UTF-8 invisible characters. + */ +fun encodeNumber(num: Long): String { + require(num >= 0) { "Only non-negative integers are supported" } + if (num == 0L) return ZERO.toString() // Represent zero as a single zero-width space + + val binary = num.toString(2) // e.g., 8 -> "1000" + val builder = StringBuilder() + for (bit in binary) { + builder.append( + if (bit == '0') ZERO // Zero Width Space + else ONE // Zero Width Non-Joiner + ) + } + return builder.toString() +} + +fun decodeWithMarker(text: String): Int? { + val markerIndex = text.indexOf(MARKER) + if (markerIndex == -1) return null // No hidden payload + val hiddenPart = text.substring(markerIndex + 1) + val binary = hiddenPart.map { + when (it) { + ZERO -> '0' + ONE -> '1' + else -> throw IllegalArgumentException("Invalid hidden character") + } + }.joinToString("") + return binary.toInt(2) +}