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) <noreply@anthropic.com>
This commit is contained in:
Benoit Marty
2026-08-26 12:55:07 +02:00
parent 373d2f1eb5
commit 2fcf806f59
3 changed files with 52 additions and 3 deletions
@@ -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)
@@ -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)
@@ -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<String, String, Unit> { _, _ -> }
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"),
)
}
}