From 2fcf806f59de756c10e4b4a02666a9f53b1086a1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 26 Aug 2026 12:55:07 +0200 Subject: [PATCH] Pretty print the push rules content before displaying it The raw push rules returned by the SDK are a single long line of json, which is hard to read on a phone. Re-encode the content with a pretty printer using a two space indentation before handing it to the file viewer. Parsing to a JsonElement and re-encoding preserves the order of the keys, so the rules are still listed in the order the server returned them. Content which cannot be parsed is passed through unchanged, so that a malformed payload remains viewable. Co-Authored-By: Claude Opus 5 (1M context) --- features/preferences/impl/build.gradle.kts | 1 + .../developer/DeveloperSettingsPresenter.kt | 21 +++++++++++- .../DeveloperSettingsPresenterTest.kt | 33 +++++++++++++++++-- 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/features/preferences/impl/build.gradle.kts b/features/preferences/impl/build.gradle.kts index 80ac4b3ba3..12554eb6fa 100644 --- a/features/preferences/impl/build.gradle.kts +++ b/features/preferences/impl/build.gradle.kts @@ -92,6 +92,7 @@ dependencies { implementation(libs.color.picker) implementation(libs.androidx.browser) implementation(libs.androidx.datastore.preferences) + implementation(libs.serialization.json) api(projects.features.preferences.api) implementation(libs.showkase) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt index 9b523d1c34..74bf595ce4 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt @@ -32,6 +32,7 @@ import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.core.data.ByteUnit +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.matrix.api.analytics.GetDatabaseSizesUseCase import io.element.android.libraries.matrix.api.core.DeviceId @@ -41,6 +42,9 @@ import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.toImmutableMap import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement @AssistedInject class DeveloperSettingsPresenter( @@ -190,7 +194,7 @@ class DeveloperSettingsPresenter( pushRulesAction.value = AsyncAction.Uninitialized navigator.openPushRules( filename = pushRulesFilename(), - content = content.orEmpty(), + content = content.orEmpty().prettyPrintJson(), ) } .onFailure { @@ -204,3 +208,18 @@ class DeveloperSettingsPresenter( */ private fun pushRulesFilename() = "push_rules${sessionId.value.replace(':', '_')}.json" } + +@OptIn(ExperimentalSerializationApi::class) +private val prettyPrintJson = Json { + prettyPrint = true + // Keep the indentation small, for a better rendering on mobile. + prettyPrintIndent = " " +} + +/** + * Pretty print this json content, so that it is readable when rendered. + * Return the content as is if it is not valid json. + */ +private fun String.prettyPrintJson(): String = runCatchingExceptions { + prettyPrintJson.encodeToString(JsonElement.serializer(), prettyPrintJson.parseToJsonElement(this)) +}.getOrDefault(this) diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt index 66f96e5eb3..b76939f72c 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt @@ -44,7 +44,17 @@ import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test -private const val A_PUSH_RULES_CONTENT = "{\"global\":{}}" +private const val A_PUSH_RULES_CONTENT = """{"global":{"override":[{"rule_id":".m.rule.master","enabled":false}]}}""" +private const val A_PUSH_RULES_CONTENT_PRETTY_PRINTED = """{ + "global": { + "override": [ + { + "rule_id": ".m.rule.master", + "enabled": false + } + ] + } +}""" private val AN_EXCEPTION = Exception("A failure") class DeveloperSettingsPresenterTest { @@ -195,7 +205,26 @@ class DeveloperSettingsPresenterTest { } openPushRulesLambda.assertions().isCalledOnce().with( value("push_rules@alice_server.org.json"), - value(A_PUSH_RULES_CONTENT), + value(A_PUSH_RULES_CONTENT_PRETTY_PRINTED), + ) + } + } + + @Test + fun `present - OpenPushRules event keeps the content as is when it is not valid json`() = runTest { + val openPushRulesLambda = lambdaRecorder { _, _ -> } + val presenter = createDeveloperSettingsPresenter( + navigator = DeveloperSettingsNavigator(openPushRulesLambda), + notificationSettingsService = FakeNotificationSettingsService( + getRawPushRulesResult = { Result.success("not json") }, + ), + ) + presenter.test { + awaitItem().eventSink(DeveloperSettingsEvent.OpenPushRules) + skipItems(2) + openPushRulesLambda.assertions().isCalledOnce().with( + value("push_rules@alice_server.org.json"), + value("not json"), ) } }