diff --git a/KtlintCustomRules/build.gradle.kts b/KtlintCustomRules/build.gradle.kts
new file mode 100644
index 000000000..d0954e2be
--- /dev/null
+++ b/KtlintCustomRules/build.gradle.kts
@@ -0,0 +1,9 @@
+
+plugins {
+ id("kotlin")
+}
+
+dependencies {
+ compileOnly(core.ktlint.cliRuleset)
+ compileOnly(core.ktlint.ruleEngineCore)
+}
diff --git a/KtlintCustomRules/proguard-rules.pro b/KtlintCustomRules/proguard-rules.pro
new file mode 100644
index 000000000..f1b424510
--- /dev/null
+++ b/KtlintCustomRules/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/CustomRuleSetProvider.kt b/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/CustomRuleSetProvider.kt
new file mode 100644
index 000000000..b834056cf
--- /dev/null
+++ b/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/CustomRuleSetProvider.kt
@@ -0,0 +1,32 @@
+/*
+ * Infomaniak Core - Android
+ * Copyright (C) 2025-2025 Infomaniak Network SA
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 com.infomaniak.core.ktlint.customrules
+
+import com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3
+import com.pinterest.ktlint.rule.engine.core.api.RuleProvider
+import com.pinterest.ktlint.rule.engine.core.api.RuleSetId
+
+internal const val CUSTOM_RULE_SET_ID = "custom-rule-set-id"
+
+class CustomRuleSetProvider : RuleSetProviderV3(RuleSetId(CUSTOM_RULE_SET_ID)) {
+ override fun getRuleProviders(): Set =
+ setOf(
+ RuleProvider { TrailingCommaInMultilineParametersRule() },
+ RuleProvider { TrailingCommaInMultilineArgumentsRule() },
+ )
+}
diff --git a/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/TrailingCommaInMultilineArgumentsRule.kt b/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/TrailingCommaInMultilineArgumentsRule.kt
new file mode 100644
index 000000000..d924e899b
--- /dev/null
+++ b/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/TrailingCommaInMultilineArgumentsRule.kt
@@ -0,0 +1,47 @@
+/*
+ * Infomaniak Core - Android
+ * Copyright (C) 2025 Infomaniak Network SA
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 com.infomaniak.core.ktlint.customrules
+
+import com.infomaniak.core.ktlint.customrules.Utils.checkTrailingComma
+import com.pinterest.ktlint.rule.engine.core.api.ElementType
+import com.pinterest.ktlint.rule.engine.core.api.Rule
+import com.pinterest.ktlint.rule.engine.core.api.RuleId
+import org.jetbrains.kotlin.com.intellij.lang.ASTNode
+
+class TrailingCommaInMultilineArgumentsRule :
+ Rule(
+ ruleId = RuleId("${CUSTOM_RULE_SET_ID}:trailing-comma-multiline-arguments"),
+ about = About()
+ ) {
+
+ override fun beforeVisitChildNodes(
+ node: ASTNode,
+ autoCorrect: Boolean,
+ emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
+ ) {
+ if (node.elementType == ElementType.VALUE_ARGUMENT_LIST) {
+ checkTrailingComma(
+ node,
+ autoCorrect,
+ emit,
+ ElementType.VALUE_ARGUMENT,
+ "Multiline argument list should have a trailing comma"
+ )
+ }
+ }
+}
diff --git a/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/TrailingCommaInMultilineParametersRule.kt b/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/TrailingCommaInMultilineParametersRule.kt
new file mode 100644
index 000000000..5e13e1678
--- /dev/null
+++ b/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/TrailingCommaInMultilineParametersRule.kt
@@ -0,0 +1,53 @@
+/*
+ * Infomaniak Core - Android
+ * Copyright (C) 2025 Infomaniak Network SA
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 com.infomaniak.core.ktlint.customrules
+
+import com.infomaniak.core.ktlint.customrules.Utils.checkTrailingComma
+import com.pinterest.ktlint.rule.engine.core.api.ElementType
+import com.pinterest.ktlint.rule.engine.core.api.Rule
+import com.pinterest.ktlint.rule.engine.core.api.RuleId
+import org.jetbrains.kotlin.com.intellij.lang.ASTNode
+import org.jetbrains.kotlin.psi.KtFunction
+
+class TrailingCommaInMultilineParametersRule :
+ Rule(
+ ruleId = RuleId("${CUSTOM_RULE_SET_ID}:trailing-comma-multiline-parameters"),
+ about = About(),
+ ) {
+
+ override fun beforeVisitChildNodes(
+ node: ASTNode,
+ autoCorrect: Boolean,
+ emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
+ ) {
+ if (isValueParameterList(node)) {
+ checkTrailingComma(
+ node,
+ autoCorrect,
+ emit,
+ ElementType.VALUE_PARAMETER,
+ "Multiline parameter list should have a trailing comma",
+ )
+ }
+ }
+
+ private fun isValueParameterList(node: ASTNode): Boolean {
+ val isValueParameterList = node.elementType == ElementType.VALUE_PARAMETER_LIST
+ return isValueParameterList && (node.psi.parent is KtFunction || node.psi.parent.parent is KtFunction)
+ }
+}
diff --git a/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/Utils.kt b/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/Utils.kt
new file mode 100644
index 000000000..6f6be7d7a
--- /dev/null
+++ b/KtlintCustomRules/src/main/java/com/infomaniak/core/ktlint/customrules/Utils.kt
@@ -0,0 +1,134 @@
+/*
+ * Infomaniak Core - Android
+ * Copyright (C) 2025 Infomaniak Network SA
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 com.infomaniak.core.ktlint.customrules
+
+import com.pinterest.ktlint.rule.engine.core.api.ElementType
+import com.pinterest.ktlint.rule.engine.core.api.ElementType.COMMA
+import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpaceWithNewline
+import org.jetbrains.kotlin.com.intellij.lang.ASTNode
+import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement
+import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.TreeElement
+import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType
+import org.jetbrains.kotlin.psi.psiUtil.children
+
+object Utils {
+
+ fun checkTrailingComma(
+ node: ASTNode,
+ autoCorrect: Boolean,
+ emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
+ elementType: IElementType,
+ errorMessage: String,
+ ) {
+ val elements = node.children().filter { it.elementType == elementType }.toList()
+ val leftParenthesis = node.findChildByType(ElementType.LPAR) ?: return
+ val rightParenthesis = node.findChildByType(ElementType.RPAR) ?: return
+
+ if (elements.size == 1) {
+ val element = elements.first()
+ val textBetweenLParAndElement = getTextBetween(leftParenthesis.treeNext, element.treePrev)
+ val textBetweenArgAndRElement = getTextBetween(element.treeNext, rightParenthesis.treePrev)
+
+ if (!(textBetweenLParAndElement.contains('\n') || textBetweenArgAndRElement.contains('\n'))) {
+ return
+ }
+ } else if (elements.isEmpty()) {
+ return
+ }
+
+ var isMultiline = false
+ var previousElementEndNode: ASTNode = leftParenthesis
+
+ for (argIndex in elements.indices) {
+ val currentElement = elements[argIndex]
+ var nodeToInspect: ASTNode? = previousElementEndNode.treeNext
+ var foundNewlineBeforeCurrentElement = false
+
+ while (nodeToInspect != null && nodeToInspect != currentElement) {
+ if (nodeToInspect.isWhiteSpaceWithNewline()) {
+ foundNewlineBeforeCurrentElement = true
+ break
+ }
+ nodeToInspect = nodeToInspect.treeNext
+ }
+
+ if (foundNewlineBeforeCurrentElement) {
+ isMultiline = true
+ break
+ }
+ previousElementEndNode = currentElement
+ }
+
+ if (!isMultiline && elements.isNotEmpty()) {
+ val lastElement = elements.last()
+ var nodeToInspect: ASTNode? = lastElement.treeNext
+
+ while (nodeToInspect != null && nodeToInspect != rightParenthesis) {
+ if (nodeToInspect.isWhiteSpaceWithNewline()) {
+ isMultiline = true
+ break
+ }
+ nodeToInspect = nodeToInspect.treeNext
+ }
+ }
+
+ if (isMultiline) {
+ val lastElement = elements.last()
+ val nextSignificantTokenAfterLastElement = lastElement.treeNext?.let { node ->
+ var current: ASTNode? = node
+ while (current != null &&
+ (
+ current.elementType == ElementType.WHITE_SPACE ||
+ current.elementType == ElementType.EOL_COMMENT ||
+ current.elementType == ElementType.BLOCK_COMMENT
+ )
+ ) {
+ current = current.treeNext
+ }
+ current
+ }
+
+ if (nextSignificantTokenAfterLastElement?.elementType != COMMA) {
+ val insertOffset = lastElement.startOffset + lastElement.textLength
+
+ emit(
+ insertOffset,
+ errorMessage,
+ autoCorrect,
+ )
+
+ if (autoCorrect) {
+ (rightParenthesis as TreeElement).rawInsertBeforeMe(LeafPsiElement(COMMA, ","))
+ }
+ }
+ }
+ }
+
+ fun getTextBetween(startNode: ASTNode?, endNode: ASTNode?): String {
+ if (startNode == null || endNode == null || startNode == endNode) return ""
+
+ return StringBuilder().apply {
+ var current: ASTNode? = startNode
+ while (current != null && current != endNode) {
+ append(current.text)
+ current = current.treeNext
+ if (current == endNode) break
+ }
+ }.toString()
+ }
+}
diff --git a/KtlintCustomRules/src/main/resources/META-INF/services/com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3 b/KtlintCustomRules/src/main/resources/META-INF/services/com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3
new file mode 100644
index 000000000..15a6710ef
--- /dev/null
+++ b/KtlintCustomRules/src/main/resources/META-INF/services/com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3
@@ -0,0 +1 @@
+com.infomaniak.core.ktlint.customrules.CustomRuleSetProvider
\ No newline at end of file
diff --git a/gradle/core.versions.toml b/gradle/core.versions.toml
index 65fabeb36..a7e346603 100644
--- a/gradle/core.versions.toml
+++ b/gradle/core.versions.toml
@@ -19,6 +19,7 @@ junitAndroidx = "1.3.0"
kotest = "5.9.1"
kotlinxCoroutines = "1.10.2"
kotlinxSerialization = "1.8.1" # Doesn't build when bumped to 1.9.0 (Waiting Kotlin 2.2.0)
+ktlint="1.7.1"
ktor = "3.2.3"
lifecycleRuntimeKtx = "2.8.7" # Doesn't build when bumped to 2.9.0 (Waiting SDK 35)
material = "1.12.0"
@@ -69,6 +70,8 @@ integrity = { module = "com.google.android.play:integrity", version.ref = "integ
kotest-assertions = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" }
+ktlint-cliRuleset = { module = "com.pinterest.ktlint:ktlint-cli-ruleset-core", version.ref = "ktlint"}
+ktlint-ruleEngineCore = { module = "com.pinterest.ktlint:ktlint-rule-engine-core", version.ref = "ktlint"}
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
kotlinx-serialization-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "kotlinxSerialization" }
ktor-client-content-negociation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }