From db40c55e7bdcca82407f6fa44dfe3b236ce71e40 Mon Sep 17 00:00:00 2001 From: andreia Date: Thu, 21 May 2026 17:20:45 +0200 Subject: [PATCH 01/15] add multiplatform DateFormatter --- .../ui/util/DateFormatter.android.kt | 26 ++++++++++++ .../groundplatform/ui/util/DateFormatter.kt | 27 ++++++++++++ .../ui/util/DateFormatter.ios.kt | 42 +++++++++++++++++++ 3 files changed, 95 insertions(+) create mode 100644 core/ui/src/androidMain/kotlin/org/groundplatform/ui/util/DateFormatter.android.kt create mode 100644 core/ui/src/commonMain/kotlin/org/groundplatform/ui/util/DateFormatter.kt create mode 100644 core/ui/src/iosMain/kotlin/org/groundplatform/ui/util/DateFormatter.ios.kt diff --git a/core/ui/src/androidMain/kotlin/org/groundplatform/ui/util/DateFormatter.android.kt b/core/ui/src/androidMain/kotlin/org/groundplatform/ui/util/DateFormatter.android.kt new file mode 100644 index 0000000000..2e2d29af68 --- /dev/null +++ b/core/ui/src/androidMain/kotlin/org/groundplatform/ui/util/DateFormatter.android.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.groundplatform.ui.util + +import java.text.DateFormat +import java.util.Date +import java.util.Locale + +actual fun formatDate(millis: Long): String = + DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.getDefault()).format(Date(millis)) + +actual fun formatTime(millis: Long): String = + DateFormat.getTimeInstance(DateFormat.SHORT, Locale.getDefault()).format(Date(millis)) diff --git a/core/ui/src/commonMain/kotlin/org/groundplatform/ui/util/DateFormatter.kt b/core/ui/src/commonMain/kotlin/org/groundplatform/ui/util/DateFormatter.kt new file mode 100644 index 0000000000..23900757b8 --- /dev/null +++ b/core/ui/src/commonMain/kotlin/org/groundplatform/ui/util/DateFormatter.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.groundplatform.ui.util + +/** + * Locale-aware date/time formatting primitives. Implemented per-platform because locale-aware + * formatting needs `java.text.DateFormat` on Android and `NSDateFormatter` on iOS. + */ + +/** Formats just the date portion of [millis] in the user's locale. */ +expect fun formatDate(millis: Long): String + +/** Formats just the time portion of [millis] in the user's locale (short style, no seconds). */ +expect fun formatTime(millis: Long): String diff --git a/core/ui/src/iosMain/kotlin/org/groundplatform/ui/util/DateFormatter.ios.kt b/core/ui/src/iosMain/kotlin/org/groundplatform/ui/util/DateFormatter.ios.kt new file mode 100644 index 0000000000..3b8e56f208 --- /dev/null +++ b/core/ui/src/iosMain/kotlin/org/groundplatform/ui/util/DateFormatter.ios.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.groundplatform.ui.util + +import kotlinx.cinterop.ExperimentalForeignApi +import platform.Foundation.NSDate +import platform.Foundation.NSDateFormatter +import platform.Foundation.NSDateFormatterMediumStyle +import platform.Foundation.NSDateFormatterNoStyle +import platform.Foundation.NSDateFormatterShortStyle +import platform.Foundation.dateWithTimeIntervalSince1970 + +@OptIn(ExperimentalForeignApi::class) +actual fun formatDate(millis: Long): String = + NSDateFormatter() + .apply { + dateStyle = NSDateFormatterMediumStyle + timeStyle = NSDateFormatterNoStyle + } + .stringFromDate(NSDate.dateWithTimeIntervalSince1970(millis / 1000.0)) + +@OptIn(ExperimentalForeignApi::class) +actual fun formatTime(millis: Long): String = + NSDateFormatter() + .apply { + dateStyle = NSDateFormatterNoStyle + timeStyle = NSDateFormatterShortStyle + } + .stringFromDate(NSDate.dateWithTimeIntervalSince1970(millis / 1000.0)) From 5aa837eb048140ab1e503040181c8b8d290c7b73 Mon Sep 17 00:00:00 2001 From: andreia Date: Mon, 25 May 2026 17:45:34 +0200 Subject: [PATCH 02/15] add formatter for each task --- .../domain/usecases/GetLoiReportUseCase.kt | 12 +-- .../domain/util/NumberFormatter.kt | 31 ++++++ .../ui/mapper/TaskValueMapper.kt | 100 ++++++++++++++++++ 3 files changed, 134 insertions(+), 9 deletions(-) create mode 100644 core/domain/src/commonMain/kotlin/org/groundplatform/domain/util/NumberFormatter.kt create mode 100644 core/ui/src/commonMain/kotlin/org/groundplatform/ui/mapper/TaskValueMapper.kt diff --git a/core/domain/src/commonMain/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCase.kt b/core/domain/src/commonMain/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCase.kt index a799d68a7d..1078b0d2f1 100644 --- a/core/domain/src/commonMain/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCase.kt +++ b/core/domain/src/commonMain/kotlin/org/groundplatform/domain/usecases/GetLoiReportUseCase.kt @@ -15,8 +15,6 @@ */ package org.groundplatform.domain.usecases -import kotlin.math.absoluteValue -import kotlin.math.round import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject @@ -35,6 +33,7 @@ import org.groundplatform.domain.model.locationofinterest.LoiReport import org.groundplatform.domain.repository.LocationOfInterestRepositoryInterface import org.groundplatform.domain.repository.SurveyRepositoryInterface import org.groundplatform.domain.repository.UserRepositoryInterface +import org.groundplatform.domain.util.toFixedDecimals /** * Use case that generates a [LoiReport] containing the LOI geometry and properties as a GeoJSON. @@ -128,12 +127,8 @@ class GetLoiReportUseCase( /** Renders this [Double] as a JSON number with exactly 6 decimal digits. */ private fun Double.roundTo6Decimals(): JsonPrimitive { - val scaled = round(this * DECIMAL_SCALE).toLong() - val sign = if (scaled < 0) "-" else "" - val absScaled = scaled.absoluteValue - val intPart = absScaled / DECIMAL_SCALE - val fracPart = (absScaled % DECIMAL_SCALE).toString().padStart(DECIMAL_DIGITS, '0') - return JsonUnquotedLiteral("$sign$intPart.$fracPart") + val value = toFixedDecimals(DECIMAL_DIGITS) + return JsonUnquotedLiteral(value) } private companion object { @@ -147,6 +142,5 @@ class GetLoiReportUseCase( const val TYPE_POLYGON = "Polygon" const val TYPE_MULTI_POLYGON = "MultiPolygon" const val DECIMAL_DIGITS = 6 - const val DECIMAL_SCALE = 1_000_000L } } diff --git a/core/domain/src/commonMain/kotlin/org/groundplatform/domain/util/NumberFormatter.kt b/core/domain/src/commonMain/kotlin/org/groundplatform/domain/util/NumberFormatter.kt new file mode 100644 index 0000000000..801fafb6be --- /dev/null +++ b/core/domain/src/commonMain/kotlin/org/groundplatform/domain/util/NumberFormatter.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.groundplatform.domain.util + +import kotlin.math.absoluteValue +import kotlin.math.round + +fun Double.toFixedDecimals(decimals: Int): String { + if (decimals <= 0) return round(this).toLong().toString() + var scale = 1L + repeat(decimals) { scale *= 10 } + val scaled = round(this * scale).toLong() + val sign = if (scaled < 0) "-" else "" + val absScaled = scaled.absoluteValue + val intPart = absScaled / scale + val fracPart = (absScaled % scale).toString().padStart(decimals, '0') + return "$sign$intPart.$fracPart" +} diff --git a/core/ui/src/commonMain/kotlin/org/groundplatform/ui/mapper/TaskValueMapper.kt b/core/ui/src/commonMain/kotlin/org/groundplatform/ui/mapper/TaskValueMapper.kt new file mode 100644 index 0000000000..3e9bd1fced --- /dev/null +++ b/core/ui/src/commonMain/kotlin/org/groundplatform/ui/mapper/TaskValueMapper.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.groundplatform.ui.mapper + +import ground_android.core.ui.generated.resources.Res +import ground_android.core.ui.generated.resources.accuracy +import ground_android.core.ui.generated.resources.altitude +import ground_android.core.ui.generated.resources.east +import ground_android.core.ui.generated.resources.north +import ground_android.core.ui.generated.resources.other +import ground_android.core.ui.generated.resources.skipped +import ground_android.core.ui.generated.resources.south +import ground_android.core.ui.generated.resources.west +import kotlin.math.absoluteValue +import kotlin.math.round +import org.groundplatform.domain.model.geometry.Point +import org.groundplatform.domain.model.submission.CaptureLocationTaskData +import org.groundplatform.domain.model.submission.DateTimeTaskData +import org.groundplatform.domain.model.submission.MultipleChoiceTaskData +import org.groundplatform.domain.model.submission.NumberTaskData +import org.groundplatform.domain.model.submission.SkippedTaskData +import org.groundplatform.domain.model.submission.TaskData +import org.groundplatform.domain.model.submission.TextTaskData +import org.groundplatform.domain.model.task.Task +import org.groundplatform.domain.util.toFixedDecimals +import org.groundplatform.ui.util.formatDate +import org.groundplatform.ui.util.formatTime +import org.jetbrains.compose.resources.getString + +object TaskValueMapper { + private const val DEGREES_DECIMALS = 6 + + /** Renders a [TaskData] value as plain text. */ + suspend fun map(task: Task, value: TaskData): String = + when (value) { + is SkippedTaskData -> getString(Res.string.skipped) + is TextTaskData -> value.text + is NumberTaskData -> value.number + is DateTimeTaskData -> formatTaskDateTime(task, value.timeInMillis) + is MultipleChoiceTaskData -> formatMultipleChoice(task, value) + is CaptureLocationTaskData -> formatCaptureLocation(value) + else -> "" + } + + /** Date for DATE tasks, time for TIME, date + time otherwise. */ + private fun formatTaskDateTime(task: Task, millis: Long): String = + when (task.type) { + Task.Type.DATE -> formatDate(millis) + Task.Type.TIME -> formatTime(millis) + else -> "${formatDate(millis)} ${formatTime(millis)}" + } + + private suspend fun formatMultipleChoice(task: Task, value: MultipleChoiceTaskData): String { + val options = task.multipleChoice?.options.orEmpty() + val selectedLabels = + value.getSelectedOptionsIdsExceptOther().map { id -> + options.firstOrNull { it.id == id }?.label ?: id + } + val withOther = + if (value.isOtherTextSelected()) { + selectedLabels + "${getString(Res.string.other)}: ${value.getOtherText()}" + } else { + selectedLabels + } + return withOther.joinToString("; ") + } + + private suspend fun formatCaptureLocation(value: CaptureLocationTaskData): String { + val lines = mutableListOf(formatPoint(value.location)) + value.altitude?.let { lines.add(getString(Res.string.altitude, formatMeters(it))) } + value.accuracy?.let { lines.add(getString(Res.string.accuracy, formatMeters(it))) } + return lines.joinToString("\n") + } + + private suspend fun formatPoint(point: Point): String { + val lat = point.coordinates.lat + val lng = point.coordinates.lng + val latDir = if (lat >= 0) getString(Res.string.north) else getString(Res.string.south) + val lngDir = if (lng >= 0) getString(Res.string.east) else getString(Res.string.west) + return "${formatDegrees(lat)} $latDir, ${formatDegrees(lng)} $lngDir" + } + + private fun formatDegrees(value: Double): String = + "${value.absoluteValue.toFixedDecimals(DEGREES_DECIMALS)}°" + + private fun formatMeters(value: Double): String = round(value).toLong().toString() +} From cfec5bb92a9856692b1179e23a48607222e383e0 Mon Sep 17 00:00:00 2001 From: andreia Date: Thu, 21 May 2026 11:00:02 +0200 Subject: [PATCH 03/15] move reusable strings to core:ui --- .../ui/datacollection/DataSubmissionConfirmationScreen.kt | 5 ++++- .../tasks/multiplechoice/MultipleChoiceItemView.kt | 7 ++++--- .../ui/home/mapcontainer/jobs/ShareLocationModal.kt | 5 ++++- app/src/main/res/values-es/strings.xml | 2 -- app/src/main/res/values-fr/strings.xml | 2 -- app/src/main/res/values-lo/strings.xml | 2 -- app/src/main/res/values-pt/strings.xml | 2 -- app/src/main/res/values-th/strings.xml | 2 -- app/src/main/res/values-vi/strings.xml | 2 -- app/src/main/res/values/strings.xml | 2 -- .../datacollection/DataSubmissionConfirmationScreenTest.kt | 6 ++++-- .../ui/home/mapcontainer/jobs/ShareLocationModalTest.kt | 4 +++- .../src/commonMain/composeResources/values-es/strings.xml | 2 ++ .../src/commonMain/composeResources/values-fr/strings.xml | 2 ++ .../src/commonMain/composeResources/values-lo/strings.xml | 2 ++ .../src/commonMain/composeResources/values-pt/strings.xml | 2 ++ .../src/commonMain/composeResources/values-th/strings.xml | 2 ++ .../src/commonMain/composeResources/values-vi/strings.xml | 2 ++ core/ui/src/commonMain/composeResources/values/strings.xml | 3 +++ 19 files changed, 34 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataSubmissionConfirmationScreen.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataSubmissionConfirmationScreen.kt index 2c02880433..6b1b24b742 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/DataSubmissionConfirmationScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/DataSubmissionConfirmationScreen.kt @@ -45,6 +45,9 @@ import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import ground_android.core.ui.generated.resources.Res +import ground_android.core.ui.generated.resources.scan_this_qr_to_download_geojson +import org.jetbrains.compose.resources.stringResource as multiplatformStringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -154,7 +157,7 @@ private fun ShareableContent(modifier: Modifier = Modifier, loiReport: LoiReport GroundQrCode( modifier = Modifier.align(Alignment.Center), title = loiReport.loiName, - footer = stringResource(R.string.scan_this_qr_to_download_geojson), + footer = multiplatformStringResource(Res.string.scan_this_qr_to_download_geojson), content = loiReport.geoJson.toString(), contentDescription = "QR code with LOI Geometry", centerLogoPainter = painterResource(R.drawable.ground_logo), diff --git a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/multiplechoice/MultipleChoiceItemView.kt b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/multiplechoice/MultipleChoiceItemView.kt index f65fb84001..288ebf34c8 100644 --- a/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/multiplechoice/MultipleChoiceItemView.kt +++ b/app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/multiplechoice/MultipleChoiceItemView.kt @@ -37,13 +37,14 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.groundplatform.android.R +import ground_android.core.ui.generated.resources.Res +import ground_android.core.ui.generated.resources.other import org.groundplatform.android.common.Constants +import org.jetbrains.compose.resources.stringResource import org.groundplatform.android.ui.common.ExcludeFromJacocoGeneratedReport import org.groundplatform.domain.model.task.MultipleChoice import org.groundplatform.domain.model.task.Option @@ -148,7 +149,7 @@ private fun OtherTextField( @Composable private fun MultipleChoiceItem.toTextLabel() = - AnnotatedString(if (isOtherOption) stringResource(id = R.string.other) else option.label) + AnnotatedString(if (isOtherOption) stringResource(Res.string.other) else option.label) @Preview(backgroundColor = 0xFFFFFFFF, showBackground = true) @Composable diff --git a/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/ShareLocationModal.kt b/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/ShareLocationModal.kt index 7056b24d96..54e9c1b782 100644 --- a/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/ShareLocationModal.kt +++ b/app/src/main/java/org/groundplatform/android/ui/home/mapcontainer/jobs/ShareLocationModal.kt @@ -38,6 +38,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import ground_android.core.ui.generated.resources.Res +import ground_android.core.ui.generated.resources.scan_this_qr_to_download_geojson +import org.jetbrains.compose.resources.stringResource as multiplatformStringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -85,7 +88,7 @@ fun ShareLocationModal(loiReport: LoiReport, onDismiss: () -> Unit) { GroundQrCode( modifier = Modifier.align(Alignment.Center), title = loiReport.loiName, - footer = stringResource(R.string.scan_this_qr_to_download_geojson), + footer = multiplatformStringResource(Res.string.scan_this_qr_to_download_geojson), content = loiReport.geoJson.toString(), contentDescription = "QR code with LOI Geometry", centerLogoPainter = painterResource(R.drawable.ground_logo), diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index c81c31665b..edcc5cabb1 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -136,7 +136,6 @@ Encuesta de solo lectura Sin presentaciones Descartar - Otro %d presentación %d presentaciones @@ -236,7 +235,6 @@ Error al guardar No se pudo guardar la foto capturada. Por favor, inténtalo de nuevo. Compartir ubicación - Escanea este código QR para ver el GeoJSON Unirse a una encuesta Código QR de encuesta no reconocido diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index da119fe15a..ffa0374d55 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -124,7 +124,6 @@ L’enquête est en lecture seule Aucune soumission Rejeter - Autres %d soumission %d soumissions @@ -217,7 +216,6 @@ Erreur d’enregistrement Impossible d’enregistrer la photo capturée. Veuillez réessayer. Partager l’emplacement - Scannez ce code QR pour afficher le GeoJson Rejoindre une enquête Code QR d’enquête non reconnu diff --git a/app/src/main/res/values-lo/strings.xml b/app/src/main/res/values-lo/strings.xml index e43b2d45db..fdf2d853fe 100644 --- a/app/src/main/res/values-lo/strings.xml +++ b/app/src/main/res/values-lo/strings.xml @@ -125,7 +125,6 @@ ແບບສຳຫຼວດເປັນແບບເບິ່ງຢ່າງດຽວ ບໍ່ມີການສົ່ງຂໍ້ມູນ ປິດແຈ້ງເຕືອນ - ອື່ນໆ %d ການສົ່ງຂໍ້ມູນ @@ -206,7 +205,6 @@ ຂໍ້ຜິດພາດໃນການບັນທຶກ ບໍ່ສາມາດບັນທຶກຮູບພາບທີ່ຖ່າຍໄດ້. ກະລຸນາລອງໃໝ່ອີກຄັ້ງ. ແບ່ງປັນຕຳແໜ່ງ - ສະແກນ QR ນີ້ເພື່ອເບິ່ງ GeoJSON ເຂົ້າຮ່ວມແບບສຳຫຼວດ ບໍ່ຮັບຮູ້ລະຫັດ QR ແບບສຳຫຼວດນີ້ diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 0366966baf..bb84fbc104 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -136,7 +136,6 @@ Inquérito apenas para leitura Nenhuma submissão Ignorar - Outro %d submissão %d submissões @@ -238,7 +237,6 @@ Erro ao salvar Falha ao salvar a foto capturada. Por favor, tente novamente. Partilhar localização - Leia este código QR para visualizar o GeoJson Aderir ao inquérito Código QR de inquérito não reconhecido diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index ea37ef3513..f6c3a5d773 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -125,7 +125,6 @@ แบบสำรวจเป็นแบบอ่านอย่างเดียว ไม่มีการส่งข้อมูล ปิดข้อความ - อื่น ๆ การส่งข้อมูล %d รายการ @@ -208,7 +207,6 @@ ข้อผิดพลาดในการบันทึก ไม่สามารถบันทึกรูปภาพที่ถ่ายได้ โปรดลองอีกครั้ง แชร์ตำแหน่ง - สแกนคิวอาร์โค้ดนี้เพื่อดู GeoJSON เข้าร่วมแบบสำรวจ ไม่รู้จักรหัสคิวอาร์ของแบบสำรวจนี้ diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 8c841f8e84..22655c74fc 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -124,7 +124,6 @@ Khảo sát chỉ được phép xem Chưa có bài gửi nào Đóng lại - Khác %d bài gửi @@ -210,7 +209,6 @@ Lỗi lưu Không thể lưu ảnh đã chụp. Vui lòng thử lại. Chia sẻ vị trí - Quét mã QR này để xem GeoJSON Tham gia khảo sát Không nhận diện được mã QR khảo sát diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1bc95ed8e0..df3210ef8d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -140,7 +140,6 @@ Survey is read-only No submissions Dismiss - Other %d submission %d submissions @@ -234,7 +233,6 @@ Re-center Share location - Scan this QR code to view the GeoJson Join survey Unrecognized survey QR code diff --git a/app/src/test/java/org/groundplatform/android/ui/datacollection/DataSubmissionConfirmationScreenTest.kt b/app/src/test/java/org/groundplatform/android/ui/datacollection/DataSubmissionConfirmationScreenTest.kt index 948e2553b0..d5c0226660 100644 --- a/app/src/test/java/org/groundplatform/android/ui/datacollection/DataSubmissionConfirmationScreenTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/datacollection/DataSubmissionConfirmationScreenTest.kt @@ -24,6 +24,8 @@ import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollTo +import ground_android.core.ui.generated.resources.Res +import ground_android.core.ui.generated.resources.scan_this_qr_to_download_geojson import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive @@ -55,7 +57,7 @@ class DataSubmissionConfirmationScreenTest { composeTestRule.onNodeWithText(LOI_REPORT.loiName).assertIsDisplayed() composeTestRule.onNodeWithTag(TEST_TAG_GROUND_QR_CODE).assertIsDisplayed() composeTestRule - .onNodeWithText(getString(R.string.scan_this_qr_to_download_geojson)) + .onNodeWithText(getString(Res.string.scan_this_qr_to_download_geojson)) .performScrollTo() .assertIsDisplayed() composeTestRule.onNodeWithText(getString(R.string.close)).performScrollTo().assertIsDisplayed() @@ -79,7 +81,7 @@ class DataSubmissionConfirmationScreenTest { composeTestRule.onNodeWithText(LOI_REPORT.loiName).assertIsDisplayed() composeTestRule.onNodeWithTag(TEST_TAG_GROUND_QR_CODE).assertIsDisplayed() composeTestRule - .onNodeWithText(getString(R.string.scan_this_qr_to_download_geojson)) + .onNodeWithText(getString(Res.string.scan_this_qr_to_download_geojson)) .assertIsDisplayed() composeTestRule.onNodeWithText(getString(R.string.close)).assertIsDisplayed() } diff --git a/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/jobs/ShareLocationModalTest.kt b/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/jobs/ShareLocationModalTest.kt index a9a7a2206f..eee9bda893 100644 --- a/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/jobs/ShareLocationModalTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/home/mapcontainer/jobs/ShareLocationModalTest.kt @@ -21,6 +21,8 @@ import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollTo +import ground_android.core.ui.generated.resources.Res +import ground_android.core.ui.generated.resources.scan_this_qr_to_download_geojson import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive @@ -49,7 +51,7 @@ class ShareLocationModalTest { composeTestRule.onNodeWithText(LOI_NAME).assertIsDisplayed() composeTestRule.onNodeWithTag(TEST_TAG_GROUND_QR_CODE).assertIsDisplayed() composeTestRule - .onNodeWithText(getString(R.string.scan_this_qr_to_download_geojson)) + .onNodeWithText(getString(Res.string.scan_this_qr_to_download_geojson)) .performScrollTo() .assertIsDisplayed() composeTestRule.onNodeWithText(getString(R.string.close)).performScrollTo().assertIsDisplayed() diff --git a/core/ui/src/commonMain/composeResources/values-es/strings.xml b/core/ui/src/commonMain/composeResources/values-es/strings.xml index e7a4cf2234..9197c46393 100644 --- a/core/ui/src/commonMain/composeResources/values-es/strings.xml +++ b/core/ui/src/commonMain/composeResources/values-es/strings.xml @@ -20,4 +20,6 @@ Compartir Sitio: %1$s Recolector de datos: %1$s + Escanea este código QR para ver el GeoJSON + Otro diff --git a/core/ui/src/commonMain/composeResources/values-fr/strings.xml b/core/ui/src/commonMain/composeResources/values-fr/strings.xml index a02bb387a4..d37a6e916c 100644 --- a/core/ui/src/commonMain/composeResources/values-fr/strings.xml +++ b/core/ui/src/commonMain/composeResources/values-fr/strings.xml @@ -19,4 +19,6 @@ Partager Site: %1$s Collecteur de données: %1$s + Scannez ce code QR pour afficher le GeoJson + Autres diff --git a/core/ui/src/commonMain/composeResources/values-lo/strings.xml b/core/ui/src/commonMain/composeResources/values-lo/strings.xml index 7cc6cc9c78..3cdf7c8e70 100644 --- a/core/ui/src/commonMain/composeResources/values-lo/strings.xml +++ b/core/ui/src/commonMain/composeResources/values-lo/strings.xml @@ -19,4 +19,6 @@ ແບ່ງປັນ ຈຸດເກັບຂໍ້ມູນ: %1$s ຜູ້ເກັບຂໍ້ມູນ: %1$s + ສະແກນ QR ນີ້ເພື່ອເບິ່ງ GeoJSON + ອື່ນໆ diff --git a/core/ui/src/commonMain/composeResources/values-pt/strings.xml b/core/ui/src/commonMain/composeResources/values-pt/strings.xml index 3e790c5acf..bac1f70831 100644 --- a/core/ui/src/commonMain/composeResources/values-pt/strings.xml +++ b/core/ui/src/commonMain/composeResources/values-pt/strings.xml @@ -20,4 +20,6 @@ Partilhar Local: %1$s Coletor de dados: %1$s + Leia este código QR para visualizar o GeoJson + Outro diff --git a/core/ui/src/commonMain/composeResources/values-th/strings.xml b/core/ui/src/commonMain/composeResources/values-th/strings.xml index fb8107eb9e..a86ef18d79 100644 --- a/core/ui/src/commonMain/composeResources/values-th/strings.xml +++ b/core/ui/src/commonMain/composeResources/values-th/strings.xml @@ -19,4 +19,6 @@ แชร์ ไซต์: %1$s ผู้เก็บข้อมูล: %1$s + สแกนคิวอาร์โค้ดนี้เพื่อดู GeoJSON + อื่น ๆ diff --git a/core/ui/src/commonMain/composeResources/values-vi/strings.xml b/core/ui/src/commonMain/composeResources/values-vi/strings.xml index dac00c6a08..fdabc6cb23 100644 --- a/core/ui/src/commonMain/composeResources/values-vi/strings.xml +++ b/core/ui/src/commonMain/composeResources/values-vi/strings.xml @@ -19,4 +19,6 @@ Chia sẻ Địa điểm: %1$s Người thu thập dữ liệu: %1$s + Quét mã QR này để xem GeoJSON + Khác diff --git a/core/ui/src/commonMain/composeResources/values/strings.xml b/core/ui/src/commonMain/composeResources/values/strings.xml index 0176966ea5..80551eb97f 100644 --- a/core/ui/src/commonMain/composeResources/values/strings.xml +++ b/core/ui/src/commonMain/composeResources/values/strings.xml @@ -20,4 +20,7 @@ Share Site: %1$s Data collector: %1$s + Scan this QR code to view the GeoJson + Skipped + Other From e8c37ca2254b1c62f030523a1a5a66317be72d04 Mon Sep 17 00:00:00 2001 From: andreia Date: Tue, 26 May 2026 10:54:57 +0200 Subject: [PATCH 04/15] update DateFormatter to be easier to test and more accurate on checking the user's 24h setting --- .../ui/util/DateFormatter.android.kt | 19 ++++++---- .../groundplatform/ui/util/DateFormatter.kt | 13 +++---- .../ui/util/DateFormatter.ios.kt | 35 +++++++++++-------- 3 files changed, 40 insertions(+), 27 deletions(-) diff --git a/core/ui/src/androidMain/kotlin/org/groundplatform/ui/util/DateFormatter.android.kt b/core/ui/src/androidMain/kotlin/org/groundplatform/ui/util/DateFormatter.android.kt index 2e2d29af68..3516b0f989 100644 --- a/core/ui/src/androidMain/kotlin/org/groundplatform/ui/util/DateFormatter.android.kt +++ b/core/ui/src/androidMain/kotlin/org/groundplatform/ui/util/DateFormatter.android.kt @@ -15,12 +15,19 @@ */ package org.groundplatform.ui.util -import java.text.DateFormat +import android.content.Context +import android.text.format.DateFormat import java.util.Date -import java.util.Locale -actual fun formatDate(millis: Long): String = - DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.getDefault()).format(Date(millis)) +/** + * Android [DateFormatter] backed by [DateFormat], which respects system date/time settings (such as + * the user's 24-hour preference) in addition to locale rules. + */ +class AndroidDateFormatter(private val context: Context) : DateFormatter { + + override fun formatDate(millis: Long): String = + DateFormat.getDateFormat(context).format(Date(millis)) -actual fun formatTime(millis: Long): String = - DateFormat.getTimeInstance(DateFormat.SHORT, Locale.getDefault()).format(Date(millis)) + override fun formatTime(millis: Long): String = + DateFormat.getTimeFormat(context).format(Date(millis)) +} diff --git a/core/ui/src/commonMain/kotlin/org/groundplatform/ui/util/DateFormatter.kt b/core/ui/src/commonMain/kotlin/org/groundplatform/ui/util/DateFormatter.kt index 23900757b8..32477236da 100644 --- a/core/ui/src/commonMain/kotlin/org/groundplatform/ui/util/DateFormatter.kt +++ b/core/ui/src/commonMain/kotlin/org/groundplatform/ui/util/DateFormatter.kt @@ -16,12 +16,13 @@ package org.groundplatform.ui.util /** - * Locale-aware date/time formatting primitives. Implemented per-platform because locale-aware - * formatting needs `java.text.DateFormat` on Android and `NSDateFormatter` on iOS. + * Locale-aware date and time formatting. Implemented as an interface so it can be injected in tests and provided per platform. */ +interface DateFormatter { -/** Formats just the date portion of [millis] in the user's locale. */ -expect fun formatDate(millis: Long): String + /** Formats just the date portion of [millis] in the user's locale (medium style). */ + fun formatDate(millis: Long): String -/** Formats just the time portion of [millis] in the user's locale (short style, no seconds). */ -expect fun formatTime(millis: Long): String + /** Formats just the time portion of [millis] in the user's locale (short style, no seconds). */ + fun formatTime(millis: Long): String +} \ No newline at end of file diff --git a/core/ui/src/iosMain/kotlin/org/groundplatform/ui/util/DateFormatter.ios.kt b/core/ui/src/iosMain/kotlin/org/groundplatform/ui/util/DateFormatter.ios.kt index 3b8e56f208..d4cd605c51 100644 --- a/core/ui/src/iosMain/kotlin/org/groundplatform/ui/util/DateFormatter.ios.kt +++ b/core/ui/src/iosMain/kotlin/org/groundplatform/ui/util/DateFormatter.ios.kt @@ -23,20 +23,25 @@ import platform.Foundation.NSDateFormatterNoStyle import platform.Foundation.NSDateFormatterShortStyle import platform.Foundation.dateWithTimeIntervalSince1970 +/** iOS [DateFormatter] backed by [NSDateFormatter], which is locale- and settings-aware. */ @OptIn(ExperimentalForeignApi::class) -actual fun formatDate(millis: Long): String = - NSDateFormatter() - .apply { - dateStyle = NSDateFormatterMediumStyle - timeStyle = NSDateFormatterNoStyle - } - .stringFromDate(NSDate.dateWithTimeIntervalSince1970(millis / 1000.0)) +class IosDateFormatter : DateFormatter { -@OptIn(ExperimentalForeignApi::class) -actual fun formatTime(millis: Long): String = - NSDateFormatter() - .apply { - dateStyle = NSDateFormatterNoStyle - timeStyle = NSDateFormatterShortStyle - } - .stringFromDate(NSDate.dateWithTimeIntervalSince1970(millis / 1000.0)) + override fun formatDate(millis: Long): String = + NSDateFormatter() + .apply { + dateStyle = NSDateFormatterMediumStyle + timeStyle = NSDateFormatterNoStyle + } + .stringFromDate(millis.toNSDate()) + + override fun formatTime(millis: Long): String = + NSDateFormatter() + .apply { + dateStyle = NSDateFormatterNoStyle + timeStyle = NSDateFormatterShortStyle + } + .stringFromDate(millis.toNSDate()) + + private fun Long.toNSDate(): NSDate = NSDate.dateWithTimeIntervalSince1970(this / 1000.0) +} From 91d50e4711732b59c476f8c25c5bd57c5294c19e Mon Sep 17 00:00:00 2001 From: andreia Date: Tue, 26 May 2026 10:55:34 +0200 Subject: [PATCH 05/15] add required dependencies for TaskValueMapper --- core/ui/build.gradle.kts | 2 ++ .../groundplatform/ui/mapper/TaskValueMapper.kt | 15 +++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 5d5429969e..3e76ab468d 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -44,6 +44,7 @@ kotlin { sourceSets { commonMain { dependencies { + implementation(project(":core:domain")) implementation(libs.compose.runtime) implementation(libs.compose.foundation) implementation(libs.compose.material3) @@ -51,6 +52,7 @@ kotlin { implementation(libs.compose.ui.tooling.preview) implementation(libs.compose.components.resources) implementation(libs.androidx.lifecycle.runtime.compose) + implementation(libs.kotlinx.collections.immutable) } } diff --git a/core/ui/src/commonMain/kotlin/org/groundplatform/ui/mapper/TaskValueMapper.kt b/core/ui/src/commonMain/kotlin/org/groundplatform/ui/mapper/TaskValueMapper.kt index 3e9bd1fced..6c2076f4af 100644 --- a/core/ui/src/commonMain/kotlin/org/groundplatform/ui/mapper/TaskValueMapper.kt +++ b/core/ui/src/commonMain/kotlin/org/groundplatform/ui/mapper/TaskValueMapper.kt @@ -36,31 +36,30 @@ import org.groundplatform.domain.model.submission.TaskData import org.groundplatform.domain.model.submission.TextTaskData import org.groundplatform.domain.model.task.Task import org.groundplatform.domain.util.toFixedDecimals -import org.groundplatform.ui.util.formatDate -import org.groundplatform.ui.util.formatTime +import org.groundplatform.ui.util.DateFormatter import org.jetbrains.compose.resources.getString object TaskValueMapper { private const val DEGREES_DECIMALS = 6 /** Renders a [TaskData] value as plain text. */ - suspend fun map(task: Task, value: TaskData): String = + suspend fun map(task: Task, value: TaskData, dateFormatter: DateFormatter): String = when (value) { is SkippedTaskData -> getString(Res.string.skipped) is TextTaskData -> value.text is NumberTaskData -> value.number - is DateTimeTaskData -> formatTaskDateTime(task, value.timeInMillis) + is DateTimeTaskData -> formatTaskDateTime(task, value.timeInMillis, dateFormatter) is MultipleChoiceTaskData -> formatMultipleChoice(task, value) is CaptureLocationTaskData -> formatCaptureLocation(value) else -> "" } /** Date for DATE tasks, time for TIME, date + time otherwise. */ - private fun formatTaskDateTime(task: Task, millis: Long): String = + private fun formatTaskDateTime(task: Task, millis: Long, dateFormatter: DateFormatter): String = when (task.type) { - Task.Type.DATE -> formatDate(millis) - Task.Type.TIME -> formatTime(millis) - else -> "${formatDate(millis)} ${formatTime(millis)}" + Task.Type.DATE -> dateFormatter.formatDate(millis) + Task.Type.TIME -> dateFormatter.formatTime(millis) + else -> "${dateFormatter.formatDate(millis)} ${dateFormatter.formatTime(millis)}" } private suspend fun formatMultipleChoice(task: Task, value: MultipleChoiceTaskData): String { From 294dbc801e997ab7538af6be8278bf38813544e7 Mon Sep 17 00:00:00 2001 From: andreia Date: Tue, 26 May 2026 11:17:03 +0200 Subject: [PATCH 06/15] add new strings for task data displayed in pdf --- .../src/commonMain/composeResources/values-es/strings.xml | 6 ++++++ .../src/commonMain/composeResources/values-fr/strings.xml | 6 ++++++ .../src/commonMain/composeResources/values-lo/strings.xml | 6 ++++++ .../src/commonMain/composeResources/values-pt/strings.xml | 6 ++++++ .../src/commonMain/composeResources/values-th/strings.xml | 6 ++++++ .../src/commonMain/composeResources/values-vi/strings.xml | 6 ++++++ .../ui/src/commonMain/composeResources/values/strings.xml | 6 ++++++ .../org/groundplatform/ui/mapper/TaskValueMapper.kt | 8 ++++---- 8 files changed, 46 insertions(+), 4 deletions(-) diff --git a/core/ui/src/commonMain/composeResources/values-es/strings.xml b/core/ui/src/commonMain/composeResources/values-es/strings.xml index 9197c46393..0898c9eeb0 100644 --- a/core/ui/src/commonMain/composeResources/values-es/strings.xml +++ b/core/ui/src/commonMain/composeResources/values-es/strings.xml @@ -22,4 +22,10 @@ Recolector de datos: %1$s Escanea este código QR para ver el GeoJSON Otro + Altitud: %1$sm + Precisión: %1$sm + N + S + E + O diff --git a/core/ui/src/commonMain/composeResources/values-fr/strings.xml b/core/ui/src/commonMain/composeResources/values-fr/strings.xml index d37a6e916c..1ede7cc019 100644 --- a/core/ui/src/commonMain/composeResources/values-fr/strings.xml +++ b/core/ui/src/commonMain/composeResources/values-fr/strings.xml @@ -21,4 +21,10 @@ Collecteur de données: %1$s Scannez ce code QR pour afficher le GeoJson Autres + Altitude: %1$sm + Précision: %1$sm + N + S + E + O diff --git a/core/ui/src/commonMain/composeResources/values-lo/strings.xml b/core/ui/src/commonMain/composeResources/values-lo/strings.xml index 3cdf7c8e70..34d1c7be79 100644 --- a/core/ui/src/commonMain/composeResources/values-lo/strings.xml +++ b/core/ui/src/commonMain/composeResources/values-lo/strings.xml @@ -21,4 +21,10 @@ ຜູ້ເກັບຂໍ້ມູນ: %1$s ສະແກນ QR ນີ້ເພື່ອເບິ່ງ GeoJSON ອື່ນໆ + ຄວາມສູງ: %1$s ແມັດ + ຄວາມແມ່ນຍຳ: %1$s ແມັດ + + + ຕ.ອ + ຕ.ຕ diff --git a/core/ui/src/commonMain/composeResources/values-pt/strings.xml b/core/ui/src/commonMain/composeResources/values-pt/strings.xml index bac1f70831..8c37606b29 100644 --- a/core/ui/src/commonMain/composeResources/values-pt/strings.xml +++ b/core/ui/src/commonMain/composeResources/values-pt/strings.xml @@ -22,4 +22,10 @@ Coletor de dados: %1$s Leia este código QR para visualizar o GeoJson Outro + Altitude: %1$sm + Precisão: %1$sm + N + S + E + O diff --git a/core/ui/src/commonMain/composeResources/values-th/strings.xml b/core/ui/src/commonMain/composeResources/values-th/strings.xml index a86ef18d79..234af07caa 100644 --- a/core/ui/src/commonMain/composeResources/values-th/strings.xml +++ b/core/ui/src/commonMain/composeResources/values-th/strings.xml @@ -21,4 +21,10 @@ ผู้เก็บข้อมูล: %1$s สแกนคิวอาร์โค้ดนี้เพื่อดู GeoJSON อื่น ๆ + ความสูง: %1$s ม. + ความแม่นยำ: %1$s ม. + ท.เหนือ + ท.ใต้ + ต.ออก + ต.ตก diff --git a/core/ui/src/commonMain/composeResources/values-vi/strings.xml b/core/ui/src/commonMain/composeResources/values-vi/strings.xml index fdabc6cb23..704137fc98 100644 --- a/core/ui/src/commonMain/composeResources/values-vi/strings.xml +++ b/core/ui/src/commonMain/composeResources/values-vi/strings.xml @@ -21,4 +21,10 @@ Người thu thập dữ liệu: %1$s Quét mã QR này để xem GeoJSON Khác + Độ cao: %1$sm + Độ chính xác: %1$sm + Bắc + Nam + Đông + Tây diff --git a/core/ui/src/commonMain/composeResources/values/strings.xml b/core/ui/src/commonMain/composeResources/values/strings.xml index 80551eb97f..d7a73b1469 100644 --- a/core/ui/src/commonMain/composeResources/values/strings.xml +++ b/core/ui/src/commonMain/composeResources/values/strings.xml @@ -23,4 +23,10 @@ Scan this QR code to view the GeoJson Skipped Other + Altitude: %1$sm + Accuracy: %1$sm + N + S + E + W diff --git a/core/ui/src/commonMain/kotlin/org/groundplatform/ui/mapper/TaskValueMapper.kt b/core/ui/src/commonMain/kotlin/org/groundplatform/ui/mapper/TaskValueMapper.kt index 6c2076f4af..48422dedab 100644 --- a/core/ui/src/commonMain/kotlin/org/groundplatform/ui/mapper/TaskValueMapper.kt +++ b/core/ui/src/commonMain/kotlin/org/groundplatform/ui/mapper/TaskValueMapper.kt @@ -16,11 +16,11 @@ package org.groundplatform.ui.mapper import ground_android.core.ui.generated.resources.Res -import ground_android.core.ui.generated.resources.accuracy -import ground_android.core.ui.generated.resources.altitude import ground_android.core.ui.generated.resources.east import ground_android.core.ui.generated.resources.north import ground_android.core.ui.generated.resources.other +import ground_android.core.ui.generated.resources.pdf_accuracy +import ground_android.core.ui.generated.resources.pdf_altitude import ground_android.core.ui.generated.resources.skipped import ground_android.core.ui.generated.resources.south import ground_android.core.ui.generated.resources.west @@ -79,8 +79,8 @@ object TaskValueMapper { private suspend fun formatCaptureLocation(value: CaptureLocationTaskData): String { val lines = mutableListOf(formatPoint(value.location)) - value.altitude?.let { lines.add(getString(Res.string.altitude, formatMeters(it))) } - value.accuracy?.let { lines.add(getString(Res.string.accuracy, formatMeters(it))) } + value.altitude?.let { lines.add(getString(Res.string.pdf_altitude, formatMeters(it))) } + value.accuracy?.let { lines.add(getString(Res.string.pdf_accuracy, formatMeters(it))) } return lines.joinToString("\n") } From 31e26ba654f98833922baca023e49ffd40da156b Mon Sep 17 00:00:00 2001 From: andreia Date: Mon, 25 May 2026 16:27:59 +0200 Subject: [PATCH 07/15] add multiplatform UI model for the pdf structure --- .../groundplatform/domain/model/task/Task.kt | 3 + .../ui/mapper/LoiReportMapper.kt | 106 ++++++++++++++++++ .../ui/model/SubmissionPdfDocument.kt | 54 +++++++++ 3 files changed, 163 insertions(+) create mode 100644 core/ui/src/commonMain/kotlin/org/groundplatform/ui/mapper/LoiReportMapper.kt create mode 100644 core/ui/src/commonMain/kotlin/org/groundplatform/ui/model/SubmissionPdfDocument.kt diff --git a/core/domain/src/commonMain/kotlin/org/groundplatform/domain/model/task/Task.kt b/core/domain/src/commonMain/kotlin/org/groundplatform/domain/model/task/Task.kt index 82169d478c..9601ba9bfd 100644 --- a/core/domain/src/commonMain/kotlin/org/groundplatform/domain/model/task/Task.kt +++ b/core/domain/src/commonMain/kotlin/org/groundplatform/domain/model/task/Task.kt @@ -49,4 +49,7 @@ data class Task( CAPTURE_LOCATION, INSTRUCTIONS, } + + fun isOmittedFromDocExport(): Boolean = + type == Type.DROP_PIN || type == Type.DRAW_AREA || type == Type.INSTRUCTIONS } diff --git a/core/ui/src/commonMain/kotlin/org/groundplatform/ui/mapper/LoiReportMapper.kt b/core/ui/src/commonMain/kotlin/org/groundplatform/ui/mapper/LoiReportMapper.kt new file mode 100644 index 0000000000..87667363c9 --- /dev/null +++ b/core/ui/src/commonMain/kotlin/org/groundplatform/ui/mapper/LoiReportMapper.kt @@ -0,0 +1,106 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.groundplatform.ui.mapper + +import ground_android.core.ui.generated.resources.Res +import ground_android.core.ui.generated.resources.job +import ground_android.core.ui.generated.resources.pdf_details_data_collector_label +import ground_android.core.ui.generated.resources.scan_this_qr_to_download_geojson +import ground_android.core.ui.generated.resources.submission +import ground_android.core.ui.generated.resources.survey +import org.groundplatform.domain.model.locationofinterest.LoiReport +import org.groundplatform.domain.model.submission.Submission +import org.groundplatform.domain.model.task.PhotoTaskData +import org.groundplatform.ui.model.SubmissionPdfDocument +import org.groundplatform.ui.model.SubmissionPdfDocument.Answer +import org.groundplatform.ui.model.SubmissionPdfDocument.Footer +import org.groundplatform.ui.model.SubmissionPdfDocument.Header +import org.groundplatform.ui.model.SubmissionPdfDocument.QrBlock +import org.groundplatform.ui.model.SubmissionPdfDocument.Row +import org.groundplatform.ui.system.pdf.PdfExportService +import org.groundplatform.ui.util.formatDate +import org.groundplatform.ui.util.formatTime +import org.jetbrains.compose.resources.getString + +object LoiReportMapper { + + suspend fun map(loiReport: LoiReport, submission: Submission): PdfExportService.Request? { + val details = loiReport.submissionDetails ?: return null + val rows = buildRows(submission) + val document = + SubmissionPdfDocument( + header = buildHeader(details, submission), + qrBlock = buildQrBlock(), + footer = buildFooter(details), + table = + SubmissionPdfDocument.Table( + submissionLabel = getString(Res.string.submission), + loiName = loiReport.loiName, + rows = rows, + ), + ) + val fileName = + listOf(loiReport.loiName, details.userName, details.dateMillis.toString()) + .joinToString("_") { it.filter(::isSafeFileChar) } + .trim('_') + + return PdfExportService.Request( + document = document, + qrContent = loiReport.geoJson.toString(), + fileName = fileName, + ) + } + + private suspend fun buildHeader( + details: LoiReport.SubmissionDetails, + submission: Submission, + ): Header = + Header( + surveyLabel = getString(Res.string.survey), + surveyName = details.surveyName, + jobLabel = getString(Res.string.job), + jobName = submission.job.name ?: submission.job.id, + timestamp = "${formatDate(details.dateMillis)} ${formatTime(details.dateMillis)}", + ) + + private suspend fun buildQrBlock(): QrBlock = + QrBlock(scanCaption = getString(Res.string.scan_this_qr_to_download_geojson)) + + private suspend fun buildFooter(details: LoiReport.SubmissionDetails): Footer = + Footer( + dataCollectorLabel = getString(Res.string.pdf_details_data_collector_label), + dataCollectorName = details.userName, + userEmail = details.userEmail, + ) + + private suspend fun buildRows(submission: Submission): List = + submission.job.tasksSorted.mapNotNull { task -> + if (task.isOmittedFromDocExport()) return@mapNotNull null + + val value = submission.data.getValue(task.id) ?: return@mapNotNull null + + Row( + question = task.label, + answer = + when (value) { + is PhotoTaskData -> Answer.Photo(value.remoteFilename) + else -> Answer.Text(TaskValueMapper.map(task, value).split("\n")) + }, + ) + } + + private fun isSafeFileChar(c: Char): Boolean = c.isLetterOrDigit() || c in "_-" +} diff --git a/core/ui/src/commonMain/kotlin/org/groundplatform/ui/model/SubmissionPdfDocument.kt b/core/ui/src/commonMain/kotlin/org/groundplatform/ui/model/SubmissionPdfDocument.kt new file mode 100644 index 0000000000..2bc0b96325 --- /dev/null +++ b/core/ui/src/commonMain/kotlin/org/groundplatform/ui/model/SubmissionPdfDocument.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.groundplatform.ui.model + +/** + * UI model for a submission PDF. Each property corresponds to a distinct visual section so that + * platform renderers (Android, iOS) can lay them out independently: + */ +data class SubmissionPdfDocument( + val header: Header, + val qrBlock: QrBlock, + val footer: Footer, + val table: Table, +) { + + data class Header( + val surveyLabel: String, + val surveyName: String, + val jobLabel: String, + val jobName: String, + val timestamp: String, + ) + + data class QrBlock(val scanCaption: String) + + data class Table(val submissionLabel: String, val loiName: String, val rows: List) + + data class Row(val question: String, val answer: Answer) + + sealed interface Answer { + data class Text(val lines: List) : Answer + + data class Photo(val remoteFilename: String) : Answer + } + + data class Footer( + val dataCollectorLabel: String, + val dataCollectorName: String, + val userEmail: String, + ) +} From 3f8304e7ac731d905968db33157811954e2ebc96 Mon Sep 17 00:00:00 2001 From: andreia Date: Tue, 26 May 2026 16:17:43 +0200 Subject: [PATCH 08/15] make TaskValueMapper map to Answer instead of String --- .../ui/mapper/TaskValueMapper.kt | 81 ++++++++++++------- 1 file changed, 50 insertions(+), 31 deletions(-) diff --git a/core/ui/src/commonMain/kotlin/org/groundplatform/ui/mapper/TaskValueMapper.kt b/core/ui/src/commonMain/kotlin/org/groundplatform/ui/mapper/TaskValueMapper.kt index 48422dedab..65361741d4 100644 --- a/core/ui/src/commonMain/kotlin/org/groundplatform/ui/mapper/TaskValueMapper.kt +++ b/core/ui/src/commonMain/kotlin/org/groundplatform/ui/mapper/TaskValueMapper.kt @@ -15,6 +15,7 @@ */ package org.groundplatform.ui.mapper +import androidx.annotation.VisibleForTesting import ground_android.core.ui.generated.resources.Res import ground_android.core.ui.generated.resources.east import ground_android.core.ui.generated.resources.north @@ -34,66 +35,84 @@ import org.groundplatform.domain.model.submission.NumberTaskData import org.groundplatform.domain.model.submission.SkippedTaskData import org.groundplatform.domain.model.submission.TaskData import org.groundplatform.domain.model.submission.TextTaskData +import org.groundplatform.domain.model.task.PhotoTaskData import org.groundplatform.domain.model.task.Task import org.groundplatform.domain.util.toFixedDecimals +import org.groundplatform.ui.model.SubmissionPdfDocument.Answer import org.groundplatform.ui.util.DateFormatter -import org.jetbrains.compose.resources.getString +import org.groundplatform.ui.util.StringResolver object TaskValueMapper { private const val DEGREES_DECIMALS = 6 - /** Renders a [TaskData] value as plain text. */ - suspend fun map(task: Task, value: TaskData, dateFormatter: DateFormatter): String = + /** Maps a [TaskData] value to the [Answer] to be rendered in the submission PDF. */ + suspend fun map( + task: Task, + value: TaskData, + dateFormatter: DateFormatter, + strings: StringResolver, + ): Answer = when (value) { - is SkippedTaskData -> getString(Res.string.skipped) - is TextTaskData -> value.text - is NumberTaskData -> value.number - is DateTimeTaskData -> formatTaskDateTime(task, value.timeInMillis, dateFormatter) - is MultipleChoiceTaskData -> formatMultipleChoice(task, value) - is CaptureLocationTaskData -> formatCaptureLocation(value) - else -> "" + is SkippedTaskData -> Answer.Text(listOf(strings.resolve(Res.string.skipped))) + is TextTaskData -> Answer.Text(listOf(value.text)) + is NumberTaskData -> Answer.Text(listOf(value.number)) + is DateTimeTaskData -> + Answer.Text(listOfNotNull(formatTaskDateTime(task, value.timeInMillis, dateFormatter))) + is MultipleChoiceTaskData -> Answer.Text(formatMultipleChoice(task, value, strings)) + is CaptureLocationTaskData -> Answer.Text(formatCaptureLocation(value, strings)) + is PhotoTaskData -> Answer.Photo(value.remoteFilename) + else -> Answer.Text(emptyList()) } - /** Date for DATE tasks, time for TIME, date + time otherwise. */ - private fun formatTaskDateTime(task: Task, millis: Long, dateFormatter: DateFormatter): String = + private fun formatTaskDateTime(task: Task, millis: Long, dateFormatter: DateFormatter): String? = when (task.type) { Task.Type.DATE -> dateFormatter.formatDate(millis) Task.Type.TIME -> dateFormatter.formatTime(millis) - else -> "${dateFormatter.formatDate(millis)} ${dateFormatter.formatTime(millis)}" + else -> null } - private suspend fun formatMultipleChoice(task: Task, value: MultipleChoiceTaskData): String { + private suspend fun formatMultipleChoice( + task: Task, + value: MultipleChoiceTaskData, + strings: StringResolver, + ): List { val options = task.multipleChoice?.options.orEmpty() val selectedLabels = value.getSelectedOptionsIdsExceptOther().map { id -> options.firstOrNull { it.id == id }?.label ?: id } - val withOther = - if (value.isOtherTextSelected()) { - selectedLabels + "${getString(Res.string.other)}: ${value.getOtherText()}" - } else { - selectedLabels - } - return withOther.joinToString("; ") + return if (value.isOtherTextSelected()) { + selectedLabels + "${strings.resolve(Res.string.other)}: ${value.getOtherText()}" + } else { + selectedLabels + } } - private suspend fun formatCaptureLocation(value: CaptureLocationTaskData): String { - val lines = mutableListOf(formatPoint(value.location)) - value.altitude?.let { lines.add(getString(Res.string.pdf_altitude, formatMeters(it))) } - value.accuracy?.let { lines.add(getString(Res.string.pdf_accuracy, formatMeters(it))) } - return lines.joinToString("\n") + /** Coordinates first, then optional altitude and accuracy lines. */ + private suspend fun formatCaptureLocation( + value: CaptureLocationTaskData, + strings: StringResolver, + ): List { + val lines = mutableListOf(formatPoint(value.location, strings)) + value.altitude?.let { lines.add(strings.resolve(Res.string.pdf_altitude, formatMeters(it))) } + value.accuracy?.let { lines.add(strings.resolve(Res.string.pdf_accuracy, formatMeters(it))) } + return lines } - private suspend fun formatPoint(point: Point): String { + private suspend fun formatPoint(point: Point, strings: StringResolver): String { val lat = point.coordinates.lat val lng = point.coordinates.lng - val latDir = if (lat >= 0) getString(Res.string.north) else getString(Res.string.south) - val lngDir = if (lng >= 0) getString(Res.string.east) else getString(Res.string.west) + val latDir = + if (lat >= 0) strings.resolve(Res.string.north) else strings.resolve(Res.string.south) + val lngDir = + if (lng >= 0) strings.resolve(Res.string.east) else strings.resolve(Res.string.west) return "${formatDegrees(lat)} $latDir, ${formatDegrees(lng)} $lngDir" } - private fun formatDegrees(value: Double): String = + @VisibleForTesting + fun formatDegrees(value: Double): String = "${value.absoluteValue.toFixedDecimals(DEGREES_DECIMALS)}°" - private fun formatMeters(value: Double): String = round(value).toLong().toString() + @VisibleForTesting + fun formatMeters(value: Double): String = round(value).toLong().toString() } From b6b2d6ca980c9dfcae3643149678a3806f054d6a Mon Sep 17 00:00:00 2001 From: andreia Date: Tue, 26 May 2026 16:27:41 +0200 Subject: [PATCH 09/15] update string resource handling and add unit tests for TaskValueMapper --- core/ui/build.gradle.kts | 10 +- .../groundplatform/ui/util/DateFormatter.kt | 5 +- .../groundplatform/ui/util/StringResolver.kt | 41 +++++ .../ui/mapper/TaskValueMapperTest.kt | 171 ++++++++++++++++++ .../ui/util/FakeDateFormatter.kt | 27 +++ .../ui/util/FakeStringResolver.kt | 29 +++ .../components/qrcode/QrCodeGenerator.jvm.kt | 23 +++ 7 files changed, 302 insertions(+), 4 deletions(-) create mode 100644 core/ui/src/commonMain/kotlin/org/groundplatform/ui/util/StringResolver.kt create mode 100644 core/ui/src/commonTest/kotlin/org/groundplatform/ui/mapper/TaskValueMapperTest.kt create mode 100644 core/ui/src/commonTest/kotlin/org/groundplatform/ui/util/FakeDateFormatter.kt create mode 100644 core/ui/src/commonTest/kotlin/org/groundplatform/ui/util/FakeStringResolver.kt create mode 100644 core/ui/src/jvmMain/kotlin/org/groundplatform/ui/components/qrcode/QrCodeGenerator.jvm.kt diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 3e76ab468d..5b23318fc2 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -32,8 +32,9 @@ kotlin { androidResources.enable = true } - val xcfName = "GroundUiKit" + jvm() + val xcfName = "GroundUiKit" listOf(iosArm64(), iosSimulatorArm64()).forEach { it.binaries.framework { baseName = xcfName @@ -56,7 +57,12 @@ kotlin { } } - commonTest { dependencies { implementation(libs.kotlin.test) } } + commonTest { + dependencies { + implementation(libs.kotlin.test) + implementation(libs.kotlinx.coroutines.test) + } + } androidMain { dependencies { implementation(libs.google.zxing) } } diff --git a/core/ui/src/commonMain/kotlin/org/groundplatform/ui/util/DateFormatter.kt b/core/ui/src/commonMain/kotlin/org/groundplatform/ui/util/DateFormatter.kt index 32477236da..aafd4ddff2 100644 --- a/core/ui/src/commonMain/kotlin/org/groundplatform/ui/util/DateFormatter.kt +++ b/core/ui/src/commonMain/kotlin/org/groundplatform/ui/util/DateFormatter.kt @@ -16,7 +16,8 @@ package org.groundplatform.ui.util /** - * Locale-aware date and time formatting. Implemented as an interface so it can be injected in tests and provided per platform. + * Locale-aware date and time formatting. Implemented as an interface so it can be injected in tests + * and provided per platform. */ interface DateFormatter { @@ -25,4 +26,4 @@ interface DateFormatter { /** Formats just the time portion of [millis] in the user's locale (short style, no seconds). */ fun formatTime(millis: Long): String -} \ No newline at end of file +} diff --git a/core/ui/src/commonMain/kotlin/org/groundplatform/ui/util/StringResolver.kt b/core/ui/src/commonMain/kotlin/org/groundplatform/ui/util/StringResolver.kt new file mode 100644 index 0000000000..1d9cd4e81c --- /dev/null +++ b/core/ui/src/commonMain/kotlin/org/groundplatform/ui/util/StringResolver.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.groundplatform.ui.util + +import org.jetbrains.compose.resources.StringResource +import org.jetbrains.compose.resources.getString + +/** + * Resolves string resources to localized text. + * + * Abstracted behind an interface so display logic can be unit-tested with a fake, without a + * Compose/Skiko resource runtime on the test classpath. + */ +interface StringResolver { + + suspend fun resolve(resource: StringResource): String + + suspend fun resolve(resource: StringResource, vararg formatArgs: Any): String +} + +/** [StringResolver] backed by Compose Multiplatform resources. */ +object ComposeStringResolver : StringResolver { + + override suspend fun resolve(resource: StringResource): String = getString(resource) + + override suspend fun resolve(resource: StringResource, vararg formatArgs: Any): String = + getString(resource, *formatArgs) +} diff --git a/core/ui/src/commonTest/kotlin/org/groundplatform/ui/mapper/TaskValueMapperTest.kt b/core/ui/src/commonTest/kotlin/org/groundplatform/ui/mapper/TaskValueMapperTest.kt new file mode 100644 index 0000000000..3fe80a949c --- /dev/null +++ b/core/ui/src/commonTest/kotlin/org/groundplatform/ui/mapper/TaskValueMapperTest.kt @@ -0,0 +1,171 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.groundplatform.ui.mapper + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.test.runTest +import org.groundplatform.domain.model.geometry.Coordinates +import org.groundplatform.domain.model.geometry.Point +import org.groundplatform.domain.model.submission.CaptureLocationTaskData +import org.groundplatform.domain.model.submission.DateTimeTaskData +import org.groundplatform.domain.model.submission.DropPinTaskData +import org.groundplatform.domain.model.submission.MultipleChoiceTaskData +import org.groundplatform.domain.model.submission.NumberTaskData +import org.groundplatform.domain.model.submission.SkippedTaskData +import org.groundplatform.domain.model.submission.TaskData +import org.groundplatform.domain.model.submission.TextTaskData +import org.groundplatform.domain.model.task.MultipleChoice +import org.groundplatform.domain.model.task.Option +import org.groundplatform.domain.model.task.PhotoTaskData +import org.groundplatform.domain.model.task.Task +import org.groundplatform.testing.FakeDataGenerator +import org.groundplatform.ui.model.SubmissionPdfDocument.Answer +import org.groundplatform.ui.util.FakeDateFormatter +import org.groundplatform.ui.util.FakeStringResolver + +class TaskValueMapperTest { + + private val dateFormatter = FakeDateFormatter + private val stringResolver = FakeStringResolver + + @Test + fun `TEXT task maps to the same value`() = runTest { + assertEquals( + Answer.Text(listOf("free text")), + map(task(Task.Type.TEXT), TextTaskData("free text")), + ) + } + + @Test + fun `NUMBER task maps to the same value`() = runTest { + assertEquals(Answer.Text(listOf("42")), map(task(Task.Type.NUMBER), NumberTaskData("42"))) + } + + @Test + fun `Skipped value renders the skipped label`() = runTest { + assertEquals(Answer.Text(listOf("skipped")), map(task(Task.Type.TEXT), SkippedTaskData())) + } + + @Test + fun `PHOTO task maps to a photo answer`() = runTest { + assertEquals( + Answer.Photo("path/to/photo.jpg"), + map(task(Task.Type.PHOTO), PhotoTaskData("path/to/photo.jpg")), + ) + } + + @Test + fun `unsupported value maps to an empty answer`() = runTest { + val value = DropPinTaskData(Point(Coordinates(1.0, 2.0))) + assertEquals(Answer.Text(emptyList()), map(task(Task.Type.DROP_PIN), value)) + } + + @Test + fun `DATE task renders date only`() = runTest { + val millis = 987654321L + assertEquals( + Answer.Text(listOf(dateFormatter.formatDate(millis))), + map(task(Task.Type.DATE), DateTimeTaskData(millis)), + ) + } + + @Test + fun `TIME task renders time only`() = runTest { + val millis = 987654321L + assertEquals( + Answer.Text(listOf(dateFormatter.formatTime(millis))), + map(task(Task.Type.TIME), DateTimeTaskData(millis)), + ) + } + + @Test + fun `non date or time task renders empty answer`() = runTest { + val millis = 987654321L + assertEquals(Answer.Text(emptyList()), map(task(Task.Type.NUMBER), DateTimeTaskData(millis))) + } + + @Test + fun `MULTIPLE_CHOICE renders each selected option label on its own line`() = runTest { + val value = MultipleChoiceTaskData(multipleChoice(), selectedOptionIds = listOf("a", "b")) + assertEquals( + Answer.Text(listOf("Apple", "Banana")), + map(task(Task.Type.MULTIPLE_CHOICE, multipleChoice()), value), + ) + } + + @Test + fun `MULTIPLE_CHOICE appends the other free text as a trailing line`() = runTest { + val other = "${MultipleChoiceTaskData.OTHER_PREFIX}custom${MultipleChoiceTaskData.OTHER_SUFFIX}" + val value = MultipleChoiceTaskData(multipleChoice(), selectedOptionIds = listOf("a", other)) + assertEquals( + Answer.Text(listOf("Apple", "other: custom")), + map(task(Task.Type.MULTIPLE_CHOICE, multipleChoice()), value), + ) + } + + @Test + fun `CAPTURE_LOCATION renders coordinates with directions and altitude and accuracy`() = runTest { + val value = + CaptureLocationTaskData(Point(Coordinates(1.5, -2.25)), altitude = 10.0, accuracy = 3.0) + + val result = map(task(Task.Type.CAPTURE_LOCATION), value) + + val expected = + Answer.Text( + listOf( + "${TaskValueMapper.formatDegrees(1.5)} north, ${TaskValueMapper.formatDegrees(2.25)} west", + "pdf_altitude(${TaskValueMapper.formatMeters(10.0)})", + "pdf_accuracy(${TaskValueMapper.formatMeters(3.0)})", + ) + ) + assertEquals(expected, result) + } + + @Test + fun `CAPTURE_LOCATION omits altitude and accuracy when absent`() = runTest { + val value = + CaptureLocationTaskData(Point(Coordinates(-1.0, 2.0)), altitude = null, accuracy = null) + + val result = map(task(Task.Type.CAPTURE_LOCATION), value) + + assertEquals( + Answer.Text( + listOf( + "${TaskValueMapper.formatDegrees(1.0)} south, ${TaskValueMapper.formatDegrees(2.0)} east" + ) + ), + result, + ) + } + + private suspend fun map(task: Task, value: TaskData) = + TaskValueMapper.map(task, value, dateFormatter, stringResolver) + + private fun task(type: Task.Type, multipleChoice: MultipleChoice? = null) = + FakeDataGenerator.newTask(type = type, multipleChoice = multipleChoice) + + private fun multipleChoice() = + MultipleChoice( + options = + persistentListOf( + Option(id = "a", code = "A", label = "Apple"), + Option(id = "b", code = "B", label = "Banana"), + ), + cardinality = MultipleChoice.Cardinality.SELECT_MULTIPLE, + ) +} diff --git a/core/ui/src/commonTest/kotlin/org/groundplatform/ui/util/FakeDateFormatter.kt b/core/ui/src/commonTest/kotlin/org/groundplatform/ui/util/FakeDateFormatter.kt new file mode 100644 index 0000000000..543ec2be30 --- /dev/null +++ b/core/ui/src/commonTest/kotlin/org/groundplatform/ui/util/FakeDateFormatter.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.groundplatform.ui.util + +/** + * [DateFormatter] for tests, so assertions don't depend on the host locale or time + * zone. + */ +object FakeDateFormatter : DateFormatter { + + override fun formatDate(millis: Long): String = "DATE($millis)" + + override fun formatTime(millis: Long): String = "TIME($millis)" +} \ No newline at end of file diff --git a/core/ui/src/commonTest/kotlin/org/groundplatform/ui/util/FakeStringResolver.kt b/core/ui/src/commonTest/kotlin/org/groundplatform/ui/util/FakeStringResolver.kt new file mode 100644 index 0000000000..251041f9db --- /dev/null +++ b/core/ui/src/commonTest/kotlin/org/groundplatform/ui/util/FakeStringResolver.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.groundplatform.ui.util + +import org.jetbrains.compose.resources.StringResource + +/** + * [StringResolver] for tests so display logic can be asserted without a Compose resource runtime. + */ +object FakeStringResolver : StringResolver { + + override suspend fun resolve(resource: StringResource): String = resource.key + + override suspend fun resolve(resource: StringResource, vararg formatArgs: Any): String = + "${resource.key}(${formatArgs.joinToString()})" +} diff --git a/core/ui/src/jvmMain/kotlin/org/groundplatform/ui/components/qrcode/QrCodeGenerator.jvm.kt b/core/ui/src/jvmMain/kotlin/org/groundplatform/ui/components/qrcode/QrCodeGenerator.jvm.kt new file mode 100644 index 0000000000..edc40ef030 --- /dev/null +++ b/core/ui/src/jvmMain/kotlin/org/groundplatform/ui/components/qrcode/QrCodeGenerator.jvm.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.groundplatform.ui.components.qrcode + +import androidx.compose.ui.graphics.ImageBitmap + +actual fun generateQrBitmap(content: String, useHighEcc: Boolean): ImageBitmap = + // The JVM target exists only to unit-test platform-independent logic, so QR generation is + // intentionally unimplemented here. + throw UnsupportedOperationException("QR code generation is not supported on the JVM target") From 5240028b76687f946b6e52fda5e6db5d5148ea3d Mon Sep 17 00:00:00 2001 From: andreia Date: Tue, 26 May 2026 17:34:28 +0200 Subject: [PATCH 10/15] add tests for string resources in core:ui --- .github/workflows/ci.yml | 2 +- core/ui/build.gradle.kts | 10 +++ .../ui/util/ComposeStringResolverTest.kt | 39 +++++++++ .../resources/robolectric.properties | 6 ++ .../composeResources/values-es/strings.xml | 1 + .../composeResources/values-fr/strings.xml | 3 +- .../composeResources/values-lo/strings.xml | 1 + .../composeResources/values-pt/strings.xml | 1 + .../composeResources/values-th/strings.xml | 1 + .../composeResources/values-vi/strings.xml | 1 + .../resources/ComposeStringResourcesTest.kt | 86 +++++++++++++++++++ 11 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 core/ui/src/androidHostTest/kotlin/org/groundplatform/ui/util/ComposeStringResolverTest.kt create mode 100644 core/ui/src/androidHostTest/resources/robolectric.properties create mode 100644 core/ui/src/jvmTest/kotlin/org/groundplatform/ui/resources/ComposeStringResourcesTest.kt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a58af7a0f2..7a24fb5633 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,7 +47,7 @@ jobs: run: ./gradlew checkCode - name: Run unit tests - run: ./gradlew testLocalDebugUnitTest jvmTest + run: ./gradlew testLocalDebugUnitTest jvmTest testAndroidHostTest - name: Generate codecov report run: ./gradlew jacocoLocalDebugUnitTestReport diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 5b23318fc2..5d71bdb988 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -30,6 +30,8 @@ kotlin { compileSdk = libs.versions.androidCompileSdk.get().toInt() minSdk = libs.versions.androidMinSdk.get().toInt() androidResources.enable = true + + withHostTest { isIncludeAndroidResources = true } } jvm() @@ -59,6 +61,7 @@ kotlin { commonTest { dependencies { + implementation(project(":core:testing")) implementation(libs.kotlin.test) implementation(libs.kotlinx.coroutines.test) } @@ -66,6 +69,13 @@ kotlin { androidMain { dependencies { implementation(libs.google.zxing) } } + val androidHostTest by getting { + dependencies { + implementation(libs.junit) + implementation(libs.robolectric) + } + } + iosMain { dependencies {} } } } diff --git a/core/ui/src/androidHostTest/kotlin/org/groundplatform/ui/util/ComposeStringResolverTest.kt b/core/ui/src/androidHostTest/kotlin/org/groundplatform/ui/util/ComposeStringResolverTest.kt new file mode 100644 index 0000000000..a880b5e1d4 --- /dev/null +++ b/core/ui/src/androidHostTest/kotlin/org/groundplatform/ui/util/ComposeStringResolverTest.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.groundplatform.ui.util + +import ground_android.core.ui.generated.resources.Res +import ground_android.core.ui.generated.resources.pdf_altitude +import ground_android.core.ui.generated.resources.skipped +import kotlin.test.assertEquals +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class ComposeStringResolverTest { + + @Test + fun `resolves a plain string`() = runTest { + assertEquals("Skipped", ComposeStringResolver.resolve(Res.string.skipped)) + } + + @Test + fun `resolves a string with a format argument`() = runTest { + assertEquals("Altitude: 5m", ComposeStringResolver.resolve(Res.string.pdf_altitude, "5")) + } +} diff --git a/core/ui/src/androidHostTest/resources/robolectric.properties b/core/ui/src/androidHostTest/resources/robolectric.properties new file mode 100644 index 0000000000..3c5d1ab94f --- /dev/null +++ b/core/ui/src/androidHostTest/resources/robolectric.properties @@ -0,0 +1,6 @@ +# src/androidHostTest/resources/robolectric.properties +# Pin Robolectric to an SDK that runs on the project's Java 17 toolchain. +# Android SDK 36 (compileSdk) requires Java 21, which the toolchain doesn't use. +# TODO: Remove the need for this file after upgrading Robolectric tests to API 33 +# Issue URL: https://github.com/google/ground-android/issues/2246 +sdk=30 \ No newline at end of file diff --git a/core/ui/src/commonMain/composeResources/values-es/strings.xml b/core/ui/src/commonMain/composeResources/values-es/strings.xml index 0898c9eeb0..1441bfe99a 100644 --- a/core/ui/src/commonMain/composeResources/values-es/strings.xml +++ b/core/ui/src/commonMain/composeResources/values-es/strings.xml @@ -21,6 +21,7 @@ Sitio: %1$s Recolector de datos: %1$s Escanea este código QR para ver el GeoJSON + Omitido Otro Altitud: %1$sm Precisión: %1$sm diff --git a/core/ui/src/commonMain/composeResources/values-fr/strings.xml b/core/ui/src/commonMain/composeResources/values-fr/strings.xml index 1ede7cc019..04072375f2 100644 --- a/core/ui/src/commonMain/composeResources/values-fr/strings.xml +++ b/core/ui/src/commonMain/composeResources/values-fr/strings.xml @@ -21,8 +21,9 @@ Collecteur de données: %1$s Scannez ce code QR pour afficher le GeoJson Autres + Ignoré Altitude: %1$sm - Précision: %1$sm + Précision: %1$sm N S E diff --git a/core/ui/src/commonMain/composeResources/values-lo/strings.xml b/core/ui/src/commonMain/composeResources/values-lo/strings.xml index 34d1c7be79..c95e27b274 100644 --- a/core/ui/src/commonMain/composeResources/values-lo/strings.xml +++ b/core/ui/src/commonMain/composeResources/values-lo/strings.xml @@ -21,6 +21,7 @@ ຜູ້ເກັບຂໍ້ມູນ: %1$s ສະແກນ QR ນີ້ເພື່ອເບິ່ງ GeoJSON ອື່ນໆ + ຂ້າມໄປແລ້ວ ຄວາມສູງ: %1$s ແມັດ ຄວາມແມ່ນຍຳ: %1$s ແມັດ diff --git a/core/ui/src/commonMain/composeResources/values-pt/strings.xml b/core/ui/src/commonMain/composeResources/values-pt/strings.xml index 8c37606b29..bc6c073d85 100644 --- a/core/ui/src/commonMain/composeResources/values-pt/strings.xml +++ b/core/ui/src/commonMain/composeResources/values-pt/strings.xml @@ -22,6 +22,7 @@ Coletor de dados: %1$s Leia este código QR para visualizar o GeoJson Outro + Ignorado Altitude: %1$sm Precisão: %1$sm N diff --git a/core/ui/src/commonMain/composeResources/values-th/strings.xml b/core/ui/src/commonMain/composeResources/values-th/strings.xml index 234af07caa..e9c343f710 100644 --- a/core/ui/src/commonMain/composeResources/values-th/strings.xml +++ b/core/ui/src/commonMain/composeResources/values-th/strings.xml @@ -21,6 +21,7 @@ ผู้เก็บข้อมูล: %1$s สแกนคิวอาร์โค้ดนี้เพื่อดู GeoJSON อื่น ๆ + ข้าม ความสูง: %1$s ม. ความแม่นยำ: %1$s ม. ท.เหนือ diff --git a/core/ui/src/commonMain/composeResources/values-vi/strings.xml b/core/ui/src/commonMain/composeResources/values-vi/strings.xml index 704137fc98..2a1ef5fe2f 100644 --- a/core/ui/src/commonMain/composeResources/values-vi/strings.xml +++ b/core/ui/src/commonMain/composeResources/values-vi/strings.xml @@ -21,6 +21,7 @@ Người thu thập dữ liệu: %1$s Quét mã QR này để xem GeoJSON Khác + Bỏ qua Độ cao: %1$sm Độ chính xác: %1$sm Bắc diff --git a/core/ui/src/jvmTest/kotlin/org/groundplatform/ui/resources/ComposeStringResourcesTest.kt b/core/ui/src/jvmTest/kotlin/org/groundplatform/ui/resources/ComposeStringResourcesTest.kt new file mode 100644 index 0000000000..86df99504a --- /dev/null +++ b/core/ui/src/jvmTest/kotlin/org/groundplatform/ui/resources/ComposeStringResourcesTest.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.groundplatform.ui.resources + +import java.io.File +import javax.xml.parsers.DocumentBuilderFactory +import kotlin.test.Test +import kotlin.test.fail +import org.w3c.dom.Element + +/** + * Validates the Compose string resources across locales. Android lint covers this for res/values, + * but these are Compose Multiplatform resources, so nothing else guards them. + */ +class ComposeStringResourcesTest { + + // Matches positional (%1$s) and non-positional (%s, %d) format specifiers. + private val placeholderRegex = Regex("""%(\d+\$)?[a-zA-Z]""") + + private val composeResourcesDir = + File("src/commonMain/composeResources").also { + require(it.isDirectory) { "Compose resources not found at ${it.absolutePath}" } + } + + @Test + fun `validate translations`() { + val default = parseStrings(File(composeResourcesDir, "values/strings.xml")) + + val issues = buildList { + for (localeDir in localeDirs()) { + val localized = parseStrings(File(localeDir, "strings.xml")) + val locale = localeDir.name + + val missing = (default.keys - localized.keys).sorted() + if (missing.isNotEmpty()) { + add("[$locale] missing keys: ${missing.joinToString()}") + } + + for ((key, value) in localized) { + val expected = default[key] ?: continue + if (placeholders(expected) != placeholders(value)) { + add( + "[$locale] \"$key\" placeholder mismatch: " + + "expected ${placeholders(expected)} but found ${placeholders(value)}" + ) + } + } + } + } + + if (issues.isNotEmpty()) { + fail("Compose string resource issues:\n" + issues.joinToString("\n")) + } + } + + private fun placeholders(value: String): List = + placeholderRegex.findAll(value).map { it.value }.sorted().toList() + + private fun localeDirs(): List = + composeResourcesDir + .listFiles { file -> file.isDirectory && file.name.startsWith("values-") } + ?.sortedBy { it.name } ?: emptyList() + + private fun parseStrings(file: File): Map { + if (!file.exists()) return emptyMap() + val doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(file) + val nodes = doc.getElementsByTagName("string") + return (0 until nodes.length).associate { + val element = nodes.item(it) as Element + element.getAttribute("name") to element.textContent + } + } +} From a776aa9be22509af118510b33c193378b8858ff2 Mon Sep 17 00:00:00 2001 From: andreia Date: Tue, 26 May 2026 17:34:46 +0200 Subject: [PATCH 11/15] add AndroidDateFormatterTest --- .../ui/util/AndroidDateFormatterTest.kt | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 core/ui/src/androidHostTest/kotlin/org/groundplatform/ui/util/AndroidDateFormatterTest.kt diff --git a/core/ui/src/androidHostTest/kotlin/org/groundplatform/ui/util/AndroidDateFormatterTest.kt b/core/ui/src/androidHostTest/kotlin/org/groundplatform/ui/util/AndroidDateFormatterTest.kt new file mode 100644 index 0000000000..1780dd8fcc --- /dev/null +++ b/core/ui/src/androidHostTest/kotlin/org/groundplatform/ui/util/AndroidDateFormatterTest.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.groundplatform.ui.util + +import java.util.Locale +import java.util.TimeZone +import kotlin.test.AfterTest +import kotlin.test.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment + +@RunWith(RobolectricTestRunner::class) +class AndroidDateFormatterTest { + + private val formatter = AndroidDateFormatter(RuntimeEnvironment.getApplication()) + + private val millis = 987654321L + private val defaultLocale = Locale.getDefault() + private val defaultZone = TimeZone.getDefault() + + @AfterTest + fun tearDown() { + Locale.setDefault(defaultLocale) + TimeZone.setDefault(defaultZone) + } + + @Test + fun `formatDate correctly renders date with US locale and UTC timezone`() { + Locale.setDefault(Locale.US) + TimeZone.setDefault(TimeZone.getTimeZone("UTC")) + assertEquals("1/12/70", formatter.formatDate(millis)) + } + + @Test + fun `formatTime correctly renders time with US locale and UTC timezone`() { + Locale.setDefault(Locale.US) + TimeZone.setDefault(TimeZone.getTimeZone("UTC")) + assertEquals("10:20 AM", formatter.formatTime(millis)) + } +} From 4db21db2439ac27e5a93787c4d2cf1f6761a6459 Mon Sep 17 00:00:00 2001 From: andreia Date: Tue, 26 May 2026 17:35:13 +0200 Subject: [PATCH 12/15] add di setup for formatter and string resolver --- .../android/di/GroundApplicationModule.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/src/main/java/org/groundplatform/android/di/GroundApplicationModule.kt b/app/src/main/java/org/groundplatform/android/di/GroundApplicationModule.kt index 220304232a..6ecfed8340 100644 --- a/app/src/main/java/org/groundplatform/android/di/GroundApplicationModule.kt +++ b/app/src/main/java/org/groundplatform/android/di/GroundApplicationModule.kt @@ -27,6 +27,10 @@ import java.util.Locale import javax.inject.Singleton import org.groundplatform.android.R import org.groundplatform.android.util.SurveyDeepLinkParser +import org.groundplatform.ui.util.AndroidDateFormatter +import org.groundplatform.ui.util.ComposeStringResolver +import org.groundplatform.ui.util.DateFormatter +import org.groundplatform.ui.util.StringResolver @InstallIn(SingletonComponent::class) @Module(includes = [ViewModelModule::class]) @@ -47,4 +51,11 @@ object GroundApplicationModule { deepLinkHost = resources.getString(R.string.deeplink_host), deepLinkPath = resources.getString(R.string.survey_deeplink_path), ) + + @Provides + @Singleton + fun provideDateFormatter(@ApplicationContext context: Context): DateFormatter = + AndroidDateFormatter(context) + + @Provides @Singleton fun provideStringResolver(): StringResolver = ComposeStringResolver } From d8b8f11a39e8092a9773170a0ce669e86f9538ef Mon Sep 17 00:00:00 2001 From: andreia Date: Tue, 26 May 2026 17:41:03 +0200 Subject: [PATCH 13/15] fix checkCode issues --- config/detekt/detekt.yml | 1532 ++++++++--------- ...oid.kt => AndroidDateFormatter.android.kt} | 0 .../ui/mapper/LoiReportMapper.kt | 106 -- .../ui/mapper/TaskValueMapper.kt | 3 +- .../ui/util/FakeDateFormatter.kt | 7 +- ...rmatter.ios.kt => IosDateFormatter.ios.kt} | 0 6 files changed, 769 insertions(+), 879 deletions(-) rename core/ui/src/androidMain/kotlin/org/groundplatform/ui/util/{DateFormatter.android.kt => AndroidDateFormatter.android.kt} (100%) delete mode 100644 core/ui/src/commonMain/kotlin/org/groundplatform/ui/mapper/LoiReportMapper.kt rename core/ui/src/iosMain/kotlin/org/groundplatform/ui/util/{DateFormatter.ios.kt => IosDateFormatter.ios.kt} (100%) diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index a13cd1ba78..3d1f2f9ad9 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -13,804 +13,804 @@ # limitations under the License. build: - maxIssues: 0 - excludeCorrectable: false - weights: - complexity: 2 - LongParameterList: 1 - style: 1 - comments: 1 + maxIssues: 0 + excludeCorrectable: false + weights: + complexity: 2 + LongParameterList: 1 + style: 1 + comments: 1 config: - validation: true - warningsAsErrors: false - checkExhaustiveness: false - # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]' - excludes: '' + validation: true + warningsAsErrors: false + checkExhaustiveness: false + # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]' + excludes: '' processors: - active: true - exclude: - - 'DetektProgressListener' - # - 'KtFileCountProcessor' - # - 'PackageCountProcessor' - # - 'ClassCountProcessor' - # - 'FunctionCountProcessor' - # - 'PropertyCountProcessor' - # - 'ProjectComplexityProcessor' - # - 'ProjectCognitiveComplexityProcessor' - # - 'ProjectLLOCProcessor' - # - 'ProjectCLOCProcessor' - # - 'ProjectLOCProcessor' - # - 'ProjectSLOCProcessor' - # - 'LicenseHeaderLoaderExtension' + active: true + exclude: + - 'DetektProgressListener' + # - 'KtFileCountProcessor' + # - 'PackageCountProcessor' + # - 'ClassCountProcessor' + # - 'FunctionCountProcessor' + # - 'PropertyCountProcessor' + # - 'ProjectComplexityProcessor' + # - 'ProjectCognitiveComplexityProcessor' + # - 'ProjectLLOCProcessor' + # - 'ProjectCLOCProcessor' + # - 'ProjectLOCProcessor' + # - 'ProjectSLOCProcessor' + # - 'LicenseHeaderLoaderExtension' console-reports: - active: true - exclude: - - 'ProjectStatisticsReport' - - 'ComplexityReport' - - 'NotificationReport' - - 'FindingsReport' - - 'FileBasedFindingsReport' - # - 'LiteFindingsReport' + active: true + exclude: + - 'ProjectStatisticsReport' + - 'ComplexityReport' + - 'NotificationReport' + - 'FindingsReport' + - 'FileBasedFindingsReport' + # - 'LiteFindingsReport' output-reports: - active: true - exclude: - - 'TxtOutputReport' - - 'XmlOutputReport' - # - 'HtmlOutputReport' - - 'MdOutputReport' - - 'SarifOutputReport' + active: true + exclude: + - 'TxtOutputReport' + - 'XmlOutputReport' + # - 'HtmlOutputReport' + - 'MdOutputReport' + - 'SarifOutputReport' comments: + active: true + AbsentOrWrongFileLicense: + active: true + licenseTemplateFile: 'license.template' + licenseTemplateIsRegex: true + CommentOverPrivateFunction: + active: false + CommentOverPrivateProperty: + active: false + DeprecatedBlockTag: + active: true + EndOfSentenceFormat: + active: true + endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)' + KDocReferencesNonPublicProperty: active: true - AbsentOrWrongFileLicense: - active: true - licenseTemplateFile: 'license.template' - licenseTemplateIsRegex: true - CommentOverPrivateFunction: - active: false - CommentOverPrivateProperty: - active: false - DeprecatedBlockTag: - active: true - EndOfSentenceFormat: - active: true - endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)' - KDocReferencesNonPublicProperty: - active: true - excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] - OutdatedDocumentation: - active: false - matchTypeParameters: true - matchDeclarationsOrder: true - allowParamOnConstructorProperties: false - UndocumentedPublicClass: - active: false - excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] - searchInNestedClass: true - searchInInnerClass: true - searchInInnerObject: true - searchInInnerInterface: true - searchInProtectedClass: false - ignoreDefaultCompanionObject: false - UndocumentedPublicFunction: - active: false - excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**', '**/*Module.kt' ] - searchProtectedFunction: false - UndocumentedPublicProperty: - active: false - excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] - searchProtectedProperty: false + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] + OutdatedDocumentation: + active: false + matchTypeParameters: true + matchDeclarationsOrder: true + allowParamOnConstructorProperties: false + UndocumentedPublicClass: + active: false + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] + searchInNestedClass: true + searchInInnerClass: true + searchInInnerObject: true + searchInInnerInterface: true + searchInProtectedClass: false + ignoreDefaultCompanionObject: false + UndocumentedPublicFunction: + active: false + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**', '**/*Module.kt' ] + searchProtectedFunction: false + UndocumentedPublicProperty: + active: false + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] + searchProtectedProperty: false complexity: + active: true + CognitiveComplexMethod: + active: true + threshold: 15 + ComplexCondition: + active: true + threshold: 5 + ComplexInterface: + active: false + threshold: 10 + includeStaticDeclarations: false + includePrivateDeclarations: false + ignoreOverloaded: false + CyclomaticComplexMethod: + active: true + threshold: 15 + ignoreSingleWhenExpression: false + ignoreSimpleWhenEntries: false + ignoreNestingFunctions: false + nestingFunctions: + - 'also' + - 'apply' + - 'forEach' + - 'isNotNull' + - 'ifNull' + - 'let' + - 'run' + - 'use' + - 'with' + LabeledExpression: + active: true + ignoredLabels: [ ] + LargeClass: + active: true + threshold: 600 + LongMethod: + active: true + threshold: 60 + LongParameterList: + active: false + functionThreshold: 6 + constructorThreshold: 7 + ignoreDefaultParameters: false + ignoreDataClasses: true + ignoreAnnotatedParameter: [ ] + MethodOverloading: active: true - CognitiveComplexMethod: - active: true - threshold: 15 - ComplexCondition: - active: true - threshold: 5 - ComplexInterface: - active: false - threshold: 10 - includeStaticDeclarations: false - includePrivateDeclarations: false - ignoreOverloaded: false - CyclomaticComplexMethod: - active: true - threshold: 15 - ignoreSingleWhenExpression: false - ignoreSimpleWhenEntries: false - ignoreNestingFunctions: false - nestingFunctions: - - 'also' - - 'apply' - - 'forEach' - - 'isNotNull' - - 'ifNull' - - 'let' - - 'run' - - 'use' - - 'with' - LabeledExpression: - active: true - ignoredLabels: [ ] - LargeClass: - active: true - threshold: 600 - LongMethod: - active: true - threshold: 60 - LongParameterList: - active: false - functionThreshold: 6 - constructorThreshold: 7 - ignoreDefaultParameters: false - ignoreDataClasses: true - ignoreAnnotatedParameter: [ ] - MethodOverloading: - active: true - threshold: 6 - NamedArguments: - active: true - threshold: 3 - ignoreArgumentsMatchingNames: false - NestedBlockDepth: - active: true - threshold: 4 - NestedScopeFunctions: - active: true - threshold: 1 - functions: - - 'kotlin.apply' - - 'kotlin.run' - - 'kotlin.with' - - 'kotlin.let' - - 'kotlin.also' - ReplaceSafeCallChainWithRun: - active: true - StringLiteralDuplication: - active: true - excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] - threshold: 3 - ignoreAnnotation: true - excludeStringsWithLessThan5Characters: true - ignoreStringsRegex: '$^' - TooManyFunctions: - active: false - excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] - thresholdInFiles: 11 - thresholdInClasses: 11 - thresholdInInterfaces: 11 - thresholdInObjects: 11 - thresholdInEnums: 11 - ignoreDeprecated: false - ignorePrivate: false - ignoreOverridden: false - ignoreAnnotatedFunctions: [ ] + threshold: 6 + NamedArguments: + active: true + threshold: 3 + ignoreArgumentsMatchingNames: false + NestedBlockDepth: + active: true + threshold: 4 + NestedScopeFunctions: + active: true + threshold: 1 + functions: + - 'kotlin.apply' + - 'kotlin.run' + - 'kotlin.with' + - 'kotlin.let' + - 'kotlin.also' + ReplaceSafeCallChainWithRun: + active: true + StringLiteralDuplication: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] + threshold: 3 + ignoreAnnotation: true + excludeStringsWithLessThan5Characters: true + ignoreStringsRegex: '$^' + TooManyFunctions: + active: false + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] + thresholdInFiles: 11 + thresholdInClasses: 11 + thresholdInInterfaces: 11 + thresholdInObjects: 11 + thresholdInEnums: 11 + ignoreDeprecated: false + ignorePrivate: false + ignoreOverridden: false + ignoreAnnotatedFunctions: [ ] coroutines: + active: true + GlobalCoroutineUsage: + active: true + InjectDispatcher: + active: true + dispatcherNames: + - 'IO' + - 'Default' + - 'Unconfined' + RedundantSuspendModifier: + active: true + SleepInsteadOfDelay: + active: true + SuspendFunSwallowedCancellation: + active: true + SuspendFunWithCoroutineScopeReceiver: + active: true + SuspendFunWithFlowReturnType: active: true - GlobalCoroutineUsage: - active: true - InjectDispatcher: - active: true - dispatcherNames: - - 'IO' - - 'Default' - - 'Unconfined' - RedundantSuspendModifier: - active: true - SleepInsteadOfDelay: - active: true - SuspendFunSwallowedCancellation: - active: true - SuspendFunWithCoroutineScopeReceiver: - active: true - SuspendFunWithFlowReturnType: - active: true empty-blocks: + active: true + EmptyCatchBlock: + active: true + allowedExceptionNameRegex: '_|(ignore|expected).*' + EmptyClassBlock: + active: true + EmptyDefaultConstructor: + active: true + EmptyDoWhileBlock: + active: true + EmptyElseBlock: + active: true + EmptyFinallyBlock: + active: true + EmptyForBlock: + active: true + EmptyFunctionBlock: + active: true + ignoreOverridden: false + EmptyIfBlock: + active: true + EmptyInitBlock: + active: true + EmptyKtFile: + active: true + EmptySecondaryConstructor: + active: true + EmptyTryBlock: + active: true + EmptyWhenBlock: + active: true + EmptyWhileBlock: active: true - EmptyCatchBlock: - active: true - allowedExceptionNameRegex: '_|(ignore|expected).*' - EmptyClassBlock: - active: true - EmptyDefaultConstructor: - active: true - EmptyDoWhileBlock: - active: true - EmptyElseBlock: - active: true - EmptyFinallyBlock: - active: true - EmptyForBlock: - active: true - EmptyFunctionBlock: - active: true - ignoreOverridden: false - EmptyIfBlock: - active: true - EmptyInitBlock: - active: true - EmptyKtFile: - active: true - EmptySecondaryConstructor: - active: true - EmptyTryBlock: - active: true - EmptyWhenBlock: - active: true - EmptyWhileBlock: - active: true exceptions: + active: true + ExceptionRaisedInUnexpectedLocation: + active: true + methodNames: + - 'equals' + - 'finalize' + - 'hashCode' + - 'toString' + InstanceOfCheckForException: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] + NotImplementedDeclaration: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] + ObjectExtendsThrowable: + active: true + PrintStackTrace: + active: true + RethrowCaughtException: + active: true + ReturnFromFinally: + active: true + ignoreLabeled: false + SwallowedException: + active: true + ignoredExceptionTypes: + - 'CancellationException' + - 'InterruptedException' + - 'MalformedURLException' + - 'NumberFormatException' + - 'ParseException' + allowedExceptionNameRegex: '_|(ignore|expected).*' + ThrowingExceptionFromFinally: active: true - ExceptionRaisedInUnexpectedLocation: - active: true - methodNames: - - 'equals' - - 'finalize' - - 'hashCode' - - 'toString' - InstanceOfCheckForException: - active: true - excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] - NotImplementedDeclaration: - active: true - excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] - ObjectExtendsThrowable: - active: true - PrintStackTrace: - active: true - RethrowCaughtException: - active: true - ReturnFromFinally: - active: true - ignoreLabeled: false - SwallowedException: - active: true - ignoredExceptionTypes: - - 'CancellationException' - - 'InterruptedException' - - 'MalformedURLException' - - 'NumberFormatException' - - 'ParseException' - allowedExceptionNameRegex: '_|(ignore|expected).*' - ThrowingExceptionFromFinally: - active: true - ThrowingExceptionInMain: - active: true - ThrowingExceptionsWithoutMessageOrCause: - active: true - excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] - exceptions: - - 'ArrayIndexOutOfBoundsException' - - 'Exception' - - 'IllegalArgumentException' - - 'IllegalMonitorStateException' - - 'IllegalStateException' - - 'IndexOutOfBoundsException' - - 'NullPointerException' - - 'RuntimeException' - - 'Throwable' - ThrowingNewInstanceOfSameException: - active: true - TooGenericExceptionCaught: - active: false - excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] - exceptionNames: - - 'ArrayIndexOutOfBoundsException' - - 'Error' - - 'Exception' - - 'IllegalMonitorStateException' - - 'IndexOutOfBoundsException' - - 'NullPointerException' - - 'RuntimeException' - - 'Throwable' - allowedExceptionNameRegex: '_|(ignore|expected).*' - TooGenericExceptionThrown: - active: true - exceptionNames: - - 'Error' - - 'Exception' - - 'RuntimeException' - - 'Throwable' + ThrowingExceptionInMain: + active: true + ThrowingExceptionsWithoutMessageOrCause: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] + exceptions: + - 'ArrayIndexOutOfBoundsException' + - 'Exception' + - 'IllegalArgumentException' + - 'IllegalMonitorStateException' + - 'IllegalStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + ThrowingNewInstanceOfSameException: + active: true + TooGenericExceptionCaught: + active: false + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + exceptionNames: + - 'ArrayIndexOutOfBoundsException' + - 'Error' + - 'Exception' + - 'IllegalMonitorStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + allowedExceptionNameRegex: '_|(ignore|expected).*' + TooGenericExceptionThrown: + active: true + exceptionNames: + - 'Error' + - 'Exception' + - 'RuntimeException' + - 'Throwable' naming: + active: true + BooleanPropertyNaming: + active: true + allowedPattern: '^(is|has|are)' + ClassNaming: + active: true + classPattern: '[A-Z][a-zA-Z0-9]*' + ConstructorParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + privateParameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + EnumNaming: + active: true + enumEntryPattern: '[A-Z][_a-zA-Z0-9]*' + ForbiddenClassName: + active: false + forbiddenName: [ ] + FunctionMaxLength: + active: false + maximumFunctionNameLength: 30 + FunctionMinLength: + active: false + minimumFunctionNameLength: 3 + FunctionNaming: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**', '**/androidHostTest/**', '**/androidUnitTest/**' ] + functionPattern: '[a-zA-Z][a-zA-Z0-9]*' + excludeClassPattern: '$^' + FunctionParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + InvalidPackageDeclaration: + active: true + rootPackage: '' + requireRootInDeclaration: false + LambdaParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*|_' + MatchingDeclarationName: active: true - BooleanPropertyNaming: - active: true - allowedPattern: '^(is|has|are)' - ClassNaming: - active: true - classPattern: '[A-Z][a-zA-Z0-9]*' - ConstructorParameterNaming: - active: true - parameterPattern: '[a-z][A-Za-z0-9]*' - privateParameterPattern: '[a-z][A-Za-z0-9]*' - excludeClassPattern: '$^' - EnumNaming: - active: true - enumEntryPattern: '[A-Z][_a-zA-Z0-9]*' - ForbiddenClassName: - active: false - forbiddenName: [ ] - FunctionMaxLength: - active: false - maximumFunctionNameLength: 30 - FunctionMinLength: - active: false - minimumFunctionNameLength: 3 - FunctionNaming: - active: true - excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] - functionPattern: '[a-zA-Z][a-zA-Z0-9]*' - excludeClassPattern: '$^' - FunctionParameterNaming: - active: true - parameterPattern: '[a-z][A-Za-z0-9]*' - excludeClassPattern: '$^' - InvalidPackageDeclaration: - active: true - rootPackage: '' - requireRootInDeclaration: false - LambdaParameterNaming: - active: true - parameterPattern: '[a-z][A-Za-z0-9]*|_' - MatchingDeclarationName: - active: true - mustBeFirst: true - multiplatformTargets: - - 'ios' - - 'android' - - 'js' - - 'jvm' - - 'native' - - 'iosArm64' - - 'iosX64' - - 'macosX64' - - 'mingwX64' - - 'linuxX64' - MemberNameEqualsClassName: - active: true - ignoreOverridden: true - NoNameShadowing: - active: true - NonBooleanPropertyPrefixedWithIs: - active: true - ObjectPropertyNaming: - active: true - constantPattern: '[A-Za-z][_A-Za-z0-9]*' - propertyPattern: '[A-Za-z][_A-Za-z0-9]*' - privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' - PackageNaming: - active: true - packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*' - TopLevelPropertyNaming: - active: true - constantPattern: '[A-Z][_A-Z0-9]*' - propertyPattern: '[A-Za-z][_A-Za-z0-9]*' - privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*' - VariableMaxLength: - active: false - maximumVariableNameLength: 64 - VariableMinLength: - active: false - minimumVariableNameLength: 1 - VariableNaming: - active: true - variablePattern: '[a-z][A-Za-z0-9]*' - privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' - excludeClassPattern: '$^' + mustBeFirst: true + multiplatformTargets: + - 'ios' + - 'android' + - 'js' + - 'jvm' + - 'native' + - 'iosArm64' + - 'iosX64' + - 'macosX64' + - 'mingwX64' + - 'linuxX64' + MemberNameEqualsClassName: + active: true + ignoreOverridden: true + NoNameShadowing: + active: true + NonBooleanPropertyPrefixedWithIs: + active: true + ObjectPropertyNaming: + active: true + constantPattern: '[A-Za-z][_A-Za-z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' + PackageNaming: + active: true + packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*' + TopLevelPropertyNaming: + active: true + constantPattern: '[A-Z][_A-Z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*' + VariableMaxLength: + active: false + maximumVariableNameLength: 64 + VariableMinLength: + active: false + minimumVariableNameLength: 1 + VariableNaming: + active: true + variablePattern: '[a-z][A-Za-z0-9]*' + privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' performance: + active: true + ArrayPrimitive: + active: true + CouldBeSequence: + active: true + threshold: 3 + ForEachOnRange: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] + SpreadOperator: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] + UnnecessaryPartOfBinaryExpression: + active: true + UnnecessaryTemporaryInstantiation: active: true - ArrayPrimitive: - active: true - CouldBeSequence: - active: true - threshold: 3 - ForEachOnRange: - active: true - excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] - SpreadOperator: - active: true - excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] - UnnecessaryPartOfBinaryExpression: - active: true - UnnecessaryTemporaryInstantiation: - active: true potential-bugs: + active: true + AvoidReferentialEquality: + active: true + forbiddenTypePatterns: + - 'kotlin.String' + CastNullableToNonNullableType: + active: true + CastToNullableType: + active: true + Deprecation: + active: true + DontDowncastCollectionTypes: + active: true + DoubleMutabilityForCollection: + active: true + mutableTypes: + - 'kotlin.collections.MutableList' + - 'kotlin.collections.MutableMap' + - 'kotlin.collections.MutableSet' + - 'java.util.ArrayList' + - 'java.util.LinkedHashSet' + - 'java.util.HashSet' + - 'java.util.LinkedHashMap' + - 'java.util.HashMap' + ElseCaseInsteadOfExhaustiveWhen: + active: true + EqualsAlwaysReturnsTrueOrFalse: + active: true + EqualsWithHashCodeExist: + active: true + ExitOutsideMain: + active: true + ExplicitGarbageCollectionCall: + active: true + HasPlatformType: + active: true + IgnoredReturnValue: + active: true + restrictToConfig: true + returnValueAnnotations: + - 'CheckResult' + - '*.CheckResult' + - 'CheckReturnValue' + - '*.CheckReturnValue' + ignoreReturnValueAnnotations: + - 'CanIgnoreReturnValue' + - '*.CanIgnoreReturnValue' + returnValueTypes: + - 'kotlin.sequences.Sequence' + - 'kotlinx.coroutines.flow.*Flow' + - 'java.util.stream.*Stream' + ignoreFunctionCall: [ ] + ImplicitDefaultLocale: + active: true + ImplicitUnitReturnType: + active: true + allowExplicitReturnType: true + InvalidRange: + active: true + IteratorHasNextCallsNextMethod: + active: true + IteratorNotThrowingNoSuchElementException: + active: true + LateinitUsage: + active: false + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] + ignoreOnClassesPattern: '' + MapGetWithNotNullAssertionOperator: + active: true + MissingPackageDeclaration: + active: true + excludes: [ '**/*.kts' ] + NullCheckOnMutableProperty: + active: true + NullableToStringCall: + active: true + PropertyUsedBeforeDeclaration: + active: true + UnconditionalJumpStatementInLoop: + active: true + UnnecessaryNotNullCheck: + active: true + UnnecessaryNotNullOperator: + active: true + UnnecessarySafeCall: + active: true + UnreachableCatchBlock: + active: true + UnreachableCode: + active: true + UnsafeCallOnNullableType: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] + UnsafeCast: + active: true + UnusedUnaryOperator: + active: true + UselessPostfixExpression: + active: true + WrongEqualsTypeParameter: active: true - AvoidReferentialEquality: - active: true - forbiddenTypePatterns: - - 'kotlin.String' - CastNullableToNonNullableType: - active: true - CastToNullableType: - active: true - Deprecation: - active: true - DontDowncastCollectionTypes: - active: true - DoubleMutabilityForCollection: - active: true - mutableTypes: - - 'kotlin.collections.MutableList' - - 'kotlin.collections.MutableMap' - - 'kotlin.collections.MutableSet' - - 'java.util.ArrayList' - - 'java.util.LinkedHashSet' - - 'java.util.HashSet' - - 'java.util.LinkedHashMap' - - 'java.util.HashMap' - ElseCaseInsteadOfExhaustiveWhen: - active: true - EqualsAlwaysReturnsTrueOrFalse: - active: true - EqualsWithHashCodeExist: - active: true - ExitOutsideMain: - active: true - ExplicitGarbageCollectionCall: - active: true - HasPlatformType: - active: true - IgnoredReturnValue: - active: true - restrictToConfig: true - returnValueAnnotations: - - 'CheckResult' - - '*.CheckResult' - - 'CheckReturnValue' - - '*.CheckReturnValue' - ignoreReturnValueAnnotations: - - 'CanIgnoreReturnValue' - - '*.CanIgnoreReturnValue' - returnValueTypes: - - 'kotlin.sequences.Sequence' - - 'kotlinx.coroutines.flow.*Flow' - - 'java.util.stream.*Stream' - ignoreFunctionCall: [ ] - ImplicitDefaultLocale: - active: true - ImplicitUnitReturnType: - active: true - allowExplicitReturnType: true - InvalidRange: - active: true - IteratorHasNextCallsNextMethod: - active: true - IteratorNotThrowingNoSuchElementException: - active: true - LateinitUsage: - active: false - excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] - ignoreOnClassesPattern: '' - MapGetWithNotNullAssertionOperator: - active: true - MissingPackageDeclaration: - active: true - excludes: [ '**/*.kts' ] - NullCheckOnMutableProperty: - active: true - NullableToStringCall: - active: true - PropertyUsedBeforeDeclaration: - active: true - UnconditionalJumpStatementInLoop: - active: true - UnnecessaryNotNullCheck: - active: true - UnnecessaryNotNullOperator: - active: true - UnnecessarySafeCall: - active: true - UnreachableCatchBlock: - active: true - UnreachableCode: - active: true - UnsafeCallOnNullableType: - active: true - excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] - UnsafeCast: - active: true - UnusedUnaryOperator: - active: true - UselessPostfixExpression: - active: true - WrongEqualsTypeParameter: - active: true style: + active: true + AlsoCouldBeApply: + active: true + BracesOnIfStatements: + active: false + singleLine: 'never' + multiLine: 'always' + BracesOnWhenStatements: + active: true + singleLine: 'necessary' + multiLine: 'consistent' + CanBeNonNullable: + active: true + CascadingCallWrapping: + active: false + includeElvis: true + ClassOrdering: + active: true + CollapsibleIfStatements: + active: true + DataClassContainsFunctions: + active: false + conversionFunctionPrefix: + - 'to' + allowOperators: false + DataClassShouldBeImmutable: + active: true + DestructuringDeclarationWithTooManyEntries: + active: true + maxDestructuringEntries: 3 + DoubleNegativeLambda: + active: true + negativeFunctions: + - reason: 'Use `takeIf` instead.' + value: 'takeUnless' + - reason: 'Use `all` instead.' + value: 'none' + negativeFunctionNameParts: + - 'not' + - 'non' + EqualsNullCall: + active: true + EqualsOnSignatureLine: + active: true + ExplicitCollectionElementAccessMethod: + active: true + ExplicitItLambdaParameter: + active: true + ExpressionBodySyntax: + active: true + includeLineWrapping: true + excludes: [ '**/*Module.kt' ] + ForbiddenAnnotation: + active: true + annotations: + - reason: 'it is a java annotation. Use `Suppress` instead.' + value: 'java.lang.SuppressWarnings' + - reason: 'it is a java annotation. Use `kotlin.Deprecated` instead.' + value: 'java.lang.Deprecated' + - reason: 'it is a java annotation. Use `kotlin.annotation.MustBeDocumented` instead.' + value: 'java.lang.annotation.Documented' + - reason: 'it is a java annotation. Use `kotlin.annotation.Target` instead.' + value: 'java.lang.annotation.Target' + - reason: 'it is a java annotation. Use `kotlin.annotation.Retention` instead.' + value: 'java.lang.annotation.Retention' + - reason: 'it is a java annotation. Use `kotlin.annotation.Repeatable` instead.' + value: 'java.lang.annotation.Repeatable' + - reason: 'Kotlin does not support @Inherited annotation, see https://youtrack.jetbrains.com/issue/KT-22265' + value: 'java.lang.annotation.Inherited' + ForbiddenComment: + active: true + comments: + - reason: 'Forbidden FIXME todo marker in comment, please fix the problem.' + value: 'FIXME:' + - reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.' + value: 'STOPSHIP:' + - reason: 'TODO comment format should be: // TODO: $description' + value: '// TODO:' + allowedPatterns: '^// TODO: .+$' + ForbiddenImport: + active: false + imports: [ ] + forbiddenPatterns: '' + ForbiddenMethodCall: + active: true + methods: + - reason: 'print does not allow you to configure the output stream. Use a logger instead.' + value: 'kotlin.io.print' + - reason: 'println does not allow you to configure the output stream. Use a logger instead.' + value: 'kotlin.io.println' + ForbiddenSuppress: + active: false + rules: [ ] + ForbiddenVoid: + active: true + ignoreOverridden: false + ignoreUsageInGenerics: false + FunctionOnlyReturningConstant: + active: true + ignoreOverridableFunction: true + ignoreActualFunction: true + excludedFunctions: [ ] + LoopWithTooManyJumpStatements: + active: true + maxJumpCount: 1 + MagicNumber: + active: false + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts' ] + ignoreNumbers: + - '-1' + - '0' + - '1' + - '2' + ignoreHashCodeFunction: true + ignorePropertyDeclaration: false + ignoreLocalVariableDeclaration: false + ignoreConstantDeclaration: true + ignoreCompanionObjectPropertyDeclaration: true + ignoreAnnotation: false + ignoreNamedArgument: true + ignoreEnums: false + ignoreRanges: false + ignoreExtensionFunctions: true + MandatoryBracesLoops: + active: false + MaxChainedCallsOnSameLine: + active: true + maxChainedCalls: 5 + MaxLineLength: + active: true + maxLineLength: 120 + excludePackageStatements: true + excludeImportStatements: true + excludeCommentStatements: false + excludeRawStrings: true + MayBeConst: + active: true + ModifierOrder: + active: true + MultilineLambdaItParameter: + active: true + MultilineRawStringIndentation: + active: true + indentSize: 4 + trimmingMethods: + - 'trimIndent' + - 'trimMargin' + NestedClassesVisibility: + active: true + NewLineAtEndOfFile: + active: true + NoTabs: + active: true + NullableBooleanCheck: + active: true + ObjectLiteralToLambda: + active: true + OptionalAbstractKeyword: + active: true + OptionalUnit: + active: true + PreferToOverPairSyntax: + active: true + ProtectedMemberInFinalClass: + active: true + RedundantExplicitType: + active: true + RedundantHigherOrderMapUsage: + active: true + RedundantVisibilityModifierRule: + active: true + ReturnCount: + active: true + max: 2 + excludedFunctions: + - 'equals' + excludeLabeled: false + excludeReturnFromLambda: true + excludeGuardClauses: true + SafeCast: + active: true + SerialVersionUIDInSerializableClass: + active: true + SpacingBetweenPackageAndImports: + active: true + StringShouldBeRawString: + active: true + maxEscapedCharacterCount: 2 + ignoredCharacters: [ ] + ThrowsCount: + active: true + max: 2 + excludeGuardClauses: false + TrailingWhitespace: + active: true + TrimMultilineRawString: + active: true + trimmingMethods: + - 'trimIndent' + - 'trimMargin' + UnderscoresInNumericLiterals: + active: false + acceptableLength: 4 + allowNonStandardGrouping: false + UnnecessaryAbstractClass: + active: true + excludes: [ '**Module**' ] + UnnecessaryAnnotationUseSiteTarget: + active: true + UnnecessaryApply: + active: true + UnnecessaryBackticks: + active: true + UnnecessaryBracesAroundTrailingLambda: + active: true + UnnecessaryFilter: + active: true + UnnecessaryInheritance: + active: true + UnnecessaryInnerClass: + active: true + UnnecessaryLet: + active: true + UnnecessaryParentheses: + active: true + allowForUnclearPrecedence: true + UntilInsteadOfRangeTo: + active: true + UnusedImports: + active: true + UnusedParameter: + active: true + allowedNames: 'ignored|expected' + UnusedPrivateClass: + active: true + UnusedPrivateMember: + active: true + allowedNames: '' + ignoreAnnotated: + - 'Preview' + UnusedPrivateProperty: + active: true + allowedNames: '_|ignored|expected|serialVersionUID' + UseAnyOrNoneInsteadOfFind: + active: true + UseArrayLiteralsInAnnotations: + active: true + UseCheckNotNull: + active: true + UseCheckOrError: + active: true + UseDataClass: + active: true + allowVars: false + UseEmptyCounterpart: + active: true + UseIfEmptyOrIfBlank: + active: true + UseIfInsteadOfWhen: + active: true + ignoreWhenContainingVariableDeclaration: false + UseIsNullOrEmpty: + active: true + UseLet: + active: true + UseOrEmpty: + active: true + UseRequire: + active: true + UseRequireNotNull: + active: true + UseSumOfInsteadOfFlatMapSize: + active: true + UselessCallOnNotNull: + active: true + UtilityClassWithPublicConstructor: + active: true + VarCouldBeVal: + active: true + ignoreLateinitVar: false + WildcardImport: active: true - AlsoCouldBeApply: - active: true - BracesOnIfStatements: - active: false - singleLine: 'never' - multiLine: 'always' - BracesOnWhenStatements: - active: true - singleLine: 'necessary' - multiLine: 'consistent' - CanBeNonNullable: - active: true - CascadingCallWrapping: - active: false - includeElvis: true - ClassOrdering: - active: true - CollapsibleIfStatements: - active: true - DataClassContainsFunctions: - active: false - conversionFunctionPrefix: - - 'to' - allowOperators: false - DataClassShouldBeImmutable: - active: true - DestructuringDeclarationWithTooManyEntries: - active: true - maxDestructuringEntries: 3 - DoubleNegativeLambda: - active: true - negativeFunctions: - - reason: 'Use `takeIf` instead.' - value: 'takeUnless' - - reason: 'Use `all` instead.' - value: 'none' - negativeFunctionNameParts: - - 'not' - - 'non' - EqualsNullCall: - active: true - EqualsOnSignatureLine: - active: true - ExplicitCollectionElementAccessMethod: - active: true - ExplicitItLambdaParameter: - active: true - ExpressionBodySyntax: - active: true - includeLineWrapping: true - excludes: [ '**/*Module.kt' ] - ForbiddenAnnotation: - active: true - annotations: - - reason: 'it is a java annotation. Use `Suppress` instead.' - value: 'java.lang.SuppressWarnings' - - reason: 'it is a java annotation. Use `kotlin.Deprecated` instead.' - value: 'java.lang.Deprecated' - - reason: 'it is a java annotation. Use `kotlin.annotation.MustBeDocumented` instead.' - value: 'java.lang.annotation.Documented' - - reason: 'it is a java annotation. Use `kotlin.annotation.Target` instead.' - value: 'java.lang.annotation.Target' - - reason: 'it is a java annotation. Use `kotlin.annotation.Retention` instead.' - value: 'java.lang.annotation.Retention' - - reason: 'it is a java annotation. Use `kotlin.annotation.Repeatable` instead.' - value: 'java.lang.annotation.Repeatable' - - reason: 'Kotlin does not support @Inherited annotation, see https://youtrack.jetbrains.com/issue/KT-22265' - value: 'java.lang.annotation.Inherited' - ForbiddenComment: - active: true - comments: - - reason: 'Forbidden FIXME todo marker in comment, please fix the problem.' - value: 'FIXME:' - - reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.' - value: 'STOPSHIP:' - - reason: 'TODO comment format should be: // TODO: $description' - value: '// TODO:' - allowedPatterns: '^// TODO: .+$' - ForbiddenImport: - active: false - imports: [ ] - forbiddenPatterns: '' - ForbiddenMethodCall: - active: true - methods: - - reason: 'print does not allow you to configure the output stream. Use a logger instead.' - value: 'kotlin.io.print' - - reason: 'println does not allow you to configure the output stream. Use a logger instead.' - value: 'kotlin.io.println' - ForbiddenSuppress: - active: false - rules: [ ] - ForbiddenVoid: - active: true - ignoreOverridden: false - ignoreUsageInGenerics: false - FunctionOnlyReturningConstant: - active: true - ignoreOverridableFunction: true - ignoreActualFunction: true - excludedFunctions: [ ] - LoopWithTooManyJumpStatements: - active: true - maxJumpCount: 1 - MagicNumber: - active: false - excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts' ] - ignoreNumbers: - - '-1' - - '0' - - '1' - - '2' - ignoreHashCodeFunction: true - ignorePropertyDeclaration: false - ignoreLocalVariableDeclaration: false - ignoreConstantDeclaration: true - ignoreCompanionObjectPropertyDeclaration: true - ignoreAnnotation: false - ignoreNamedArgument: true - ignoreEnums: false - ignoreRanges: false - ignoreExtensionFunctions: true - MandatoryBracesLoops: - active: false - MaxChainedCallsOnSameLine: - active: true - maxChainedCalls: 5 - MaxLineLength: - active: true - maxLineLength: 120 - excludePackageStatements: true - excludeImportStatements: true - excludeCommentStatements: false - excludeRawStrings: true - MayBeConst: - active: true - ModifierOrder: - active: true - MultilineLambdaItParameter: - active: true - MultilineRawStringIndentation: - active: true - indentSize: 4 - trimmingMethods: - - 'trimIndent' - - 'trimMargin' - NestedClassesVisibility: - active: true - NewLineAtEndOfFile: - active: true - NoTabs: - active: true - NullableBooleanCheck: - active: true - ObjectLiteralToLambda: - active: true - OptionalAbstractKeyword: - active: true - OptionalUnit: - active: true - PreferToOverPairSyntax: - active: true - ProtectedMemberInFinalClass: - active: true - RedundantExplicitType: - active: true - RedundantHigherOrderMapUsage: - active: true - RedundantVisibilityModifierRule: - active: true - ReturnCount: - active: true - max: 2 - excludedFunctions: - - 'equals' - excludeLabeled: false - excludeReturnFromLambda: true - excludeGuardClauses: true - SafeCast: - active: true - SerialVersionUIDInSerializableClass: - active: true - SpacingBetweenPackageAndImports: - active: true - StringShouldBeRawString: - active: true - maxEscapedCharacterCount: 2 - ignoredCharacters: [ ] - ThrowsCount: - active: true - max: 2 - excludeGuardClauses: false - TrailingWhitespace: - active: true - TrimMultilineRawString: - active: true - trimmingMethods: - - 'trimIndent' - - 'trimMargin' - UnderscoresInNumericLiterals: - active: false - acceptableLength: 4 - allowNonStandardGrouping: false - UnnecessaryAbstractClass: - active: true - excludes: [ '**Module**' ] - UnnecessaryAnnotationUseSiteTarget: - active: true - UnnecessaryApply: - active: true - UnnecessaryBackticks: - active: true - UnnecessaryBracesAroundTrailingLambda: - active: true - UnnecessaryFilter: - active: true - UnnecessaryInheritance: - active: true - UnnecessaryInnerClass: - active: true - UnnecessaryLet: - active: true - UnnecessaryParentheses: - active: true - allowForUnclearPrecedence: true - UntilInsteadOfRangeTo: - active: true - UnusedImports: - active: true - UnusedParameter: - active: true - allowedNames: 'ignored|expected' - UnusedPrivateClass: - active: true - UnusedPrivateMember: - active: true - allowedNames: '' - ignoreAnnotated: - - 'Preview' - UnusedPrivateProperty: - active: true - allowedNames: '_|ignored|expected|serialVersionUID' - UseAnyOrNoneInsteadOfFind: - active: true - UseArrayLiteralsInAnnotations: - active: true - UseCheckNotNull: - active: true - UseCheckOrError: - active: true - UseDataClass: - active: true - allowVars: false - UseEmptyCounterpart: - active: true - UseIfEmptyOrIfBlank: - active: true - UseIfInsteadOfWhen: - active: true - ignoreWhenContainingVariableDeclaration: false - UseIsNullOrEmpty: - active: true - UseLet: - active: true - UseOrEmpty: - active: true - UseRequire: - active: true - UseRequireNotNull: - active: true - UseSumOfInsteadOfFlatMapSize: - active: true - UselessCallOnNotNull: - active: true - UtilityClassWithPublicConstructor: - active: true - VarCouldBeVal: - active: true - ignoreLateinitVar: false - WildcardImport: - active: true - excludeImports: - - 'java.util.*' + excludeImports: + - 'java.util.*' diff --git a/core/ui/src/androidMain/kotlin/org/groundplatform/ui/util/DateFormatter.android.kt b/core/ui/src/androidMain/kotlin/org/groundplatform/ui/util/AndroidDateFormatter.android.kt similarity index 100% rename from core/ui/src/androidMain/kotlin/org/groundplatform/ui/util/DateFormatter.android.kt rename to core/ui/src/androidMain/kotlin/org/groundplatform/ui/util/AndroidDateFormatter.android.kt diff --git a/core/ui/src/commonMain/kotlin/org/groundplatform/ui/mapper/LoiReportMapper.kt b/core/ui/src/commonMain/kotlin/org/groundplatform/ui/mapper/LoiReportMapper.kt deleted file mode 100644 index 87667363c9..0000000000 --- a/core/ui/src/commonMain/kotlin/org/groundplatform/ui/mapper/LoiReportMapper.kt +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright 2026 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.groundplatform.ui.mapper - -import ground_android.core.ui.generated.resources.Res -import ground_android.core.ui.generated.resources.job -import ground_android.core.ui.generated.resources.pdf_details_data_collector_label -import ground_android.core.ui.generated.resources.scan_this_qr_to_download_geojson -import ground_android.core.ui.generated.resources.submission -import ground_android.core.ui.generated.resources.survey -import org.groundplatform.domain.model.locationofinterest.LoiReport -import org.groundplatform.domain.model.submission.Submission -import org.groundplatform.domain.model.task.PhotoTaskData -import org.groundplatform.ui.model.SubmissionPdfDocument -import org.groundplatform.ui.model.SubmissionPdfDocument.Answer -import org.groundplatform.ui.model.SubmissionPdfDocument.Footer -import org.groundplatform.ui.model.SubmissionPdfDocument.Header -import org.groundplatform.ui.model.SubmissionPdfDocument.QrBlock -import org.groundplatform.ui.model.SubmissionPdfDocument.Row -import org.groundplatform.ui.system.pdf.PdfExportService -import org.groundplatform.ui.util.formatDate -import org.groundplatform.ui.util.formatTime -import org.jetbrains.compose.resources.getString - -object LoiReportMapper { - - suspend fun map(loiReport: LoiReport, submission: Submission): PdfExportService.Request? { - val details = loiReport.submissionDetails ?: return null - val rows = buildRows(submission) - val document = - SubmissionPdfDocument( - header = buildHeader(details, submission), - qrBlock = buildQrBlock(), - footer = buildFooter(details), - table = - SubmissionPdfDocument.Table( - submissionLabel = getString(Res.string.submission), - loiName = loiReport.loiName, - rows = rows, - ), - ) - val fileName = - listOf(loiReport.loiName, details.userName, details.dateMillis.toString()) - .joinToString("_") { it.filter(::isSafeFileChar) } - .trim('_') - - return PdfExportService.Request( - document = document, - qrContent = loiReport.geoJson.toString(), - fileName = fileName, - ) - } - - private suspend fun buildHeader( - details: LoiReport.SubmissionDetails, - submission: Submission, - ): Header = - Header( - surveyLabel = getString(Res.string.survey), - surveyName = details.surveyName, - jobLabel = getString(Res.string.job), - jobName = submission.job.name ?: submission.job.id, - timestamp = "${formatDate(details.dateMillis)} ${formatTime(details.dateMillis)}", - ) - - private suspend fun buildQrBlock(): QrBlock = - QrBlock(scanCaption = getString(Res.string.scan_this_qr_to_download_geojson)) - - private suspend fun buildFooter(details: LoiReport.SubmissionDetails): Footer = - Footer( - dataCollectorLabel = getString(Res.string.pdf_details_data_collector_label), - dataCollectorName = details.userName, - userEmail = details.userEmail, - ) - - private suspend fun buildRows(submission: Submission): List = - submission.job.tasksSorted.mapNotNull { task -> - if (task.isOmittedFromDocExport()) return@mapNotNull null - - val value = submission.data.getValue(task.id) ?: return@mapNotNull null - - Row( - question = task.label, - answer = - when (value) { - is PhotoTaskData -> Answer.Photo(value.remoteFilename) - else -> Answer.Text(TaskValueMapper.map(task, value).split("\n")) - }, - ) - } - - private fun isSafeFileChar(c: Char): Boolean = c.isLetterOrDigit() || c in "_-" -} diff --git a/core/ui/src/commonMain/kotlin/org/groundplatform/ui/mapper/TaskValueMapper.kt b/core/ui/src/commonMain/kotlin/org/groundplatform/ui/mapper/TaskValueMapper.kt index 65361741d4..3eb7447e38 100644 --- a/core/ui/src/commonMain/kotlin/org/groundplatform/ui/mapper/TaskValueMapper.kt +++ b/core/ui/src/commonMain/kotlin/org/groundplatform/ui/mapper/TaskValueMapper.kt @@ -113,6 +113,5 @@ object TaskValueMapper { fun formatDegrees(value: Double): String = "${value.absoluteValue.toFixedDecimals(DEGREES_DECIMALS)}°" - @VisibleForTesting - fun formatMeters(value: Double): String = round(value).toLong().toString() + @VisibleForTesting fun formatMeters(value: Double): String = round(value).toLong().toString() } diff --git a/core/ui/src/commonTest/kotlin/org/groundplatform/ui/util/FakeDateFormatter.kt b/core/ui/src/commonTest/kotlin/org/groundplatform/ui/util/FakeDateFormatter.kt index 543ec2be30..a22a47e26d 100644 --- a/core/ui/src/commonTest/kotlin/org/groundplatform/ui/util/FakeDateFormatter.kt +++ b/core/ui/src/commonTest/kotlin/org/groundplatform/ui/util/FakeDateFormatter.kt @@ -15,13 +15,10 @@ */ package org.groundplatform.ui.util -/** - * [DateFormatter] for tests, so assertions don't depend on the host locale or time - * zone. - */ +/** [DateFormatter] for tests, so assertions don't depend on the host locale or time zone. */ object FakeDateFormatter : DateFormatter { override fun formatDate(millis: Long): String = "DATE($millis)" override fun formatTime(millis: Long): String = "TIME($millis)" -} \ No newline at end of file +} diff --git a/core/ui/src/iosMain/kotlin/org/groundplatform/ui/util/DateFormatter.ios.kt b/core/ui/src/iosMain/kotlin/org/groundplatform/ui/util/IosDateFormatter.ios.kt similarity index 100% rename from core/ui/src/iosMain/kotlin/org/groundplatform/ui/util/DateFormatter.ios.kt rename to core/ui/src/iosMain/kotlin/org/groundplatform/ui/util/IosDateFormatter.ios.kt From aaef86ee4866f75109c0f3d8463d5108d7a382fa Mon Sep 17 00:00:00 2001 From: andreia Date: Tue, 26 May 2026 18:01:45 +0200 Subject: [PATCH 14/15] remove unneeded changes --- config/detekt/detekt.yml | 1532 +++++++++++++++++++------------------- 1 file changed, 766 insertions(+), 766 deletions(-) diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index 3d1f2f9ad9..596ad02e9d 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -13,804 +13,804 @@ # limitations under the License. build: - maxIssues: 0 - excludeCorrectable: false - weights: - complexity: 2 - LongParameterList: 1 - style: 1 - comments: 1 + maxIssues: 0 + excludeCorrectable: false + weights: + complexity: 2 + LongParameterList: 1 + style: 1 + comments: 1 config: - validation: true - warningsAsErrors: false - checkExhaustiveness: false - # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]' - excludes: '' + validation: true + warningsAsErrors: false + checkExhaustiveness: false + # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]' + excludes: '' processors: - active: true - exclude: - - 'DetektProgressListener' - # - 'KtFileCountProcessor' - # - 'PackageCountProcessor' - # - 'ClassCountProcessor' - # - 'FunctionCountProcessor' - # - 'PropertyCountProcessor' - # - 'ProjectComplexityProcessor' - # - 'ProjectCognitiveComplexityProcessor' - # - 'ProjectLLOCProcessor' - # - 'ProjectCLOCProcessor' - # - 'ProjectLOCProcessor' - # - 'ProjectSLOCProcessor' - # - 'LicenseHeaderLoaderExtension' + active: true + exclude: + - 'DetektProgressListener' + # - 'KtFileCountProcessor' + # - 'PackageCountProcessor' + # - 'ClassCountProcessor' + # - 'FunctionCountProcessor' + # - 'PropertyCountProcessor' + # - 'ProjectComplexityProcessor' + # - 'ProjectCognitiveComplexityProcessor' + # - 'ProjectLLOCProcessor' + # - 'ProjectCLOCProcessor' + # - 'ProjectLOCProcessor' + # - 'ProjectSLOCProcessor' + # - 'LicenseHeaderLoaderExtension' console-reports: - active: true - exclude: - - 'ProjectStatisticsReport' - - 'ComplexityReport' - - 'NotificationReport' - - 'FindingsReport' - - 'FileBasedFindingsReport' - # - 'LiteFindingsReport' + active: true + exclude: + - 'ProjectStatisticsReport' + - 'ComplexityReport' + - 'NotificationReport' + - 'FindingsReport' + - 'FileBasedFindingsReport' + # - 'LiteFindingsReport' output-reports: - active: true - exclude: - - 'TxtOutputReport' - - 'XmlOutputReport' - # - 'HtmlOutputReport' - - 'MdOutputReport' - - 'SarifOutputReport' + active: true + exclude: + - 'TxtOutputReport' + - 'XmlOutputReport' + # - 'HtmlOutputReport' + - 'MdOutputReport' + - 'SarifOutputReport' comments: - active: true - AbsentOrWrongFileLicense: - active: true - licenseTemplateFile: 'license.template' - licenseTemplateIsRegex: true - CommentOverPrivateFunction: - active: false - CommentOverPrivateProperty: - active: false - DeprecatedBlockTag: - active: true - EndOfSentenceFormat: - active: true - endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)' - KDocReferencesNonPublicProperty: active: true - excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] - OutdatedDocumentation: - active: false - matchTypeParameters: true - matchDeclarationsOrder: true - allowParamOnConstructorProperties: false - UndocumentedPublicClass: - active: false - excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] - searchInNestedClass: true - searchInInnerClass: true - searchInInnerObject: true - searchInInnerInterface: true - searchInProtectedClass: false - ignoreDefaultCompanionObject: false - UndocumentedPublicFunction: - active: false - excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**', '**/*Module.kt' ] - searchProtectedFunction: false - UndocumentedPublicProperty: - active: false - excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] - searchProtectedProperty: false + AbsentOrWrongFileLicense: + active: true + licenseTemplateFile: 'license.template' + licenseTemplateIsRegex: true + CommentOverPrivateFunction: + active: false + CommentOverPrivateProperty: + active: false + DeprecatedBlockTag: + active: true + EndOfSentenceFormat: + active: true + endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)' + KDocReferencesNonPublicProperty: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] + OutdatedDocumentation: + active: false + matchTypeParameters: true + matchDeclarationsOrder: true + allowParamOnConstructorProperties: false + UndocumentedPublicClass: + active: false + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] + searchInNestedClass: true + searchInInnerClass: true + searchInInnerObject: true + searchInInnerInterface: true + searchInProtectedClass: false + ignoreDefaultCompanionObject: false + UndocumentedPublicFunction: + active: false + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**', '**/*Module.kt' ] + searchProtectedFunction: false + UndocumentedPublicProperty: + active: false + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] + searchProtectedProperty: false complexity: - active: true - CognitiveComplexMethod: - active: true - threshold: 15 - ComplexCondition: - active: true - threshold: 5 - ComplexInterface: - active: false - threshold: 10 - includeStaticDeclarations: false - includePrivateDeclarations: false - ignoreOverloaded: false - CyclomaticComplexMethod: - active: true - threshold: 15 - ignoreSingleWhenExpression: false - ignoreSimpleWhenEntries: false - ignoreNestingFunctions: false - nestingFunctions: - - 'also' - - 'apply' - - 'forEach' - - 'isNotNull' - - 'ifNull' - - 'let' - - 'run' - - 'use' - - 'with' - LabeledExpression: - active: true - ignoredLabels: [ ] - LargeClass: - active: true - threshold: 600 - LongMethod: - active: true - threshold: 60 - LongParameterList: - active: false - functionThreshold: 6 - constructorThreshold: 7 - ignoreDefaultParameters: false - ignoreDataClasses: true - ignoreAnnotatedParameter: [ ] - MethodOverloading: active: true - threshold: 6 - NamedArguments: - active: true - threshold: 3 - ignoreArgumentsMatchingNames: false - NestedBlockDepth: - active: true - threshold: 4 - NestedScopeFunctions: - active: true - threshold: 1 - functions: - - 'kotlin.apply' - - 'kotlin.run' - - 'kotlin.with' - - 'kotlin.let' - - 'kotlin.also' - ReplaceSafeCallChainWithRun: - active: true - StringLiteralDuplication: - active: true - excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] - threshold: 3 - ignoreAnnotation: true - excludeStringsWithLessThan5Characters: true - ignoreStringsRegex: '$^' - TooManyFunctions: - active: false - excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] - thresholdInFiles: 11 - thresholdInClasses: 11 - thresholdInInterfaces: 11 - thresholdInObjects: 11 - thresholdInEnums: 11 - ignoreDeprecated: false - ignorePrivate: false - ignoreOverridden: false - ignoreAnnotatedFunctions: [ ] + CognitiveComplexMethod: + active: true + threshold: 15 + ComplexCondition: + active: true + threshold: 5 + ComplexInterface: + active: false + threshold: 10 + includeStaticDeclarations: false + includePrivateDeclarations: false + ignoreOverloaded: false + CyclomaticComplexMethod: + active: true + threshold: 15 + ignoreSingleWhenExpression: false + ignoreSimpleWhenEntries: false + ignoreNestingFunctions: false + nestingFunctions: + - 'also' + - 'apply' + - 'forEach' + - 'isNotNull' + - 'ifNull' + - 'let' + - 'run' + - 'use' + - 'with' + LabeledExpression: + active: true + ignoredLabels: [ ] + LargeClass: + active: true + threshold: 600 + LongMethod: + active: true + threshold: 60 + LongParameterList: + active: false + functionThreshold: 6 + constructorThreshold: 7 + ignoreDefaultParameters: false + ignoreDataClasses: true + ignoreAnnotatedParameter: [ ] + MethodOverloading: + active: true + threshold: 6 + NamedArguments: + active: true + threshold: 3 + ignoreArgumentsMatchingNames: false + NestedBlockDepth: + active: true + threshold: 4 + NestedScopeFunctions: + active: true + threshold: 1 + functions: + - 'kotlin.apply' + - 'kotlin.run' + - 'kotlin.with' + - 'kotlin.let' + - 'kotlin.also' + ReplaceSafeCallChainWithRun: + active: true + StringLiteralDuplication: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] + threshold: 3 + ignoreAnnotation: true + excludeStringsWithLessThan5Characters: true + ignoreStringsRegex: '$^' + TooManyFunctions: + active: false + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] + thresholdInFiles: 11 + thresholdInClasses: 11 + thresholdInInterfaces: 11 + thresholdInObjects: 11 + thresholdInEnums: 11 + ignoreDeprecated: false + ignorePrivate: false + ignoreOverridden: false + ignoreAnnotatedFunctions: [ ] coroutines: - active: true - GlobalCoroutineUsage: - active: true - InjectDispatcher: - active: true - dispatcherNames: - - 'IO' - - 'Default' - - 'Unconfined' - RedundantSuspendModifier: - active: true - SleepInsteadOfDelay: - active: true - SuspendFunSwallowedCancellation: - active: true - SuspendFunWithCoroutineScopeReceiver: - active: true - SuspendFunWithFlowReturnType: active: true + GlobalCoroutineUsage: + active: true + InjectDispatcher: + active: true + dispatcherNames: + - 'IO' + - 'Default' + - 'Unconfined' + RedundantSuspendModifier: + active: true + SleepInsteadOfDelay: + active: true + SuspendFunSwallowedCancellation: + active: true + SuspendFunWithCoroutineScopeReceiver: + active: true + SuspendFunWithFlowReturnType: + active: true empty-blocks: - active: true - EmptyCatchBlock: - active: true - allowedExceptionNameRegex: '_|(ignore|expected).*' - EmptyClassBlock: - active: true - EmptyDefaultConstructor: - active: true - EmptyDoWhileBlock: - active: true - EmptyElseBlock: - active: true - EmptyFinallyBlock: - active: true - EmptyForBlock: - active: true - EmptyFunctionBlock: - active: true - ignoreOverridden: false - EmptyIfBlock: - active: true - EmptyInitBlock: - active: true - EmptyKtFile: - active: true - EmptySecondaryConstructor: - active: true - EmptyTryBlock: - active: true - EmptyWhenBlock: - active: true - EmptyWhileBlock: active: true + EmptyCatchBlock: + active: true + allowedExceptionNameRegex: '_|(ignore|expected).*' + EmptyClassBlock: + active: true + EmptyDefaultConstructor: + active: true + EmptyDoWhileBlock: + active: true + EmptyElseBlock: + active: true + EmptyFinallyBlock: + active: true + EmptyForBlock: + active: true + EmptyFunctionBlock: + active: true + ignoreOverridden: false + EmptyIfBlock: + active: true + EmptyInitBlock: + active: true + EmptyKtFile: + active: true + EmptySecondaryConstructor: + active: true + EmptyTryBlock: + active: true + EmptyWhenBlock: + active: true + EmptyWhileBlock: + active: true exceptions: - active: true - ExceptionRaisedInUnexpectedLocation: - active: true - methodNames: - - 'equals' - - 'finalize' - - 'hashCode' - - 'toString' - InstanceOfCheckForException: - active: true - excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] - NotImplementedDeclaration: - active: true - excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] - ObjectExtendsThrowable: - active: true - PrintStackTrace: - active: true - RethrowCaughtException: - active: true - ReturnFromFinally: - active: true - ignoreLabeled: false - SwallowedException: - active: true - ignoredExceptionTypes: - - 'CancellationException' - - 'InterruptedException' - - 'MalformedURLException' - - 'NumberFormatException' - - 'ParseException' - allowedExceptionNameRegex: '_|(ignore|expected).*' - ThrowingExceptionFromFinally: active: true - ThrowingExceptionInMain: - active: true - ThrowingExceptionsWithoutMessageOrCause: - active: true - excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] - exceptions: - - 'ArrayIndexOutOfBoundsException' - - 'Exception' - - 'IllegalArgumentException' - - 'IllegalMonitorStateException' - - 'IllegalStateException' - - 'IndexOutOfBoundsException' - - 'NullPointerException' - - 'RuntimeException' - - 'Throwable' - ThrowingNewInstanceOfSameException: - active: true - TooGenericExceptionCaught: - active: false - excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] - exceptionNames: - - 'ArrayIndexOutOfBoundsException' - - 'Error' - - 'Exception' - - 'IllegalMonitorStateException' - - 'IndexOutOfBoundsException' - - 'NullPointerException' - - 'RuntimeException' - - 'Throwable' - allowedExceptionNameRegex: '_|(ignore|expected).*' - TooGenericExceptionThrown: - active: true - exceptionNames: - - 'Error' - - 'Exception' - - 'RuntimeException' - - 'Throwable' + ExceptionRaisedInUnexpectedLocation: + active: true + methodNames: + - 'equals' + - 'finalize' + - 'hashCode' + - 'toString' + InstanceOfCheckForException: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] + NotImplementedDeclaration: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] + ObjectExtendsThrowable: + active: true + PrintStackTrace: + active: true + RethrowCaughtException: + active: true + ReturnFromFinally: + active: true + ignoreLabeled: false + SwallowedException: + active: true + ignoredExceptionTypes: + - 'CancellationException' + - 'InterruptedException' + - 'MalformedURLException' + - 'NumberFormatException' + - 'ParseException' + allowedExceptionNameRegex: '_|(ignore|expected).*' + ThrowingExceptionFromFinally: + active: true + ThrowingExceptionInMain: + active: true + ThrowingExceptionsWithoutMessageOrCause: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] + exceptions: + - 'ArrayIndexOutOfBoundsException' + - 'Exception' + - 'IllegalArgumentException' + - 'IllegalMonitorStateException' + - 'IllegalStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + ThrowingNewInstanceOfSameException: + active: true + TooGenericExceptionCaught: + active: false + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**' ] + exceptionNames: + - 'ArrayIndexOutOfBoundsException' + - 'Error' + - 'Exception' + - 'IllegalMonitorStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + allowedExceptionNameRegex: '_|(ignore|expected).*' + TooGenericExceptionThrown: + active: true + exceptionNames: + - 'Error' + - 'Exception' + - 'RuntimeException' + - 'Throwable' naming: - active: true - BooleanPropertyNaming: - active: true - allowedPattern: '^(is|has|are)' - ClassNaming: - active: true - classPattern: '[A-Z][a-zA-Z0-9]*' - ConstructorParameterNaming: - active: true - parameterPattern: '[a-z][A-Za-z0-9]*' - privateParameterPattern: '[a-z][A-Za-z0-9]*' - excludeClassPattern: '$^' - EnumNaming: - active: true - enumEntryPattern: '[A-Z][_a-zA-Z0-9]*' - ForbiddenClassName: - active: false - forbiddenName: [ ] - FunctionMaxLength: - active: false - maximumFunctionNameLength: 30 - FunctionMinLength: - active: false - minimumFunctionNameLength: 3 - FunctionNaming: - active: true - excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**', '**/androidHostTest/**', '**/androidUnitTest/**' ] - functionPattern: '[a-zA-Z][a-zA-Z0-9]*' - excludeClassPattern: '$^' - FunctionParameterNaming: - active: true - parameterPattern: '[a-z][A-Za-z0-9]*' - excludeClassPattern: '$^' - InvalidPackageDeclaration: - active: true - rootPackage: '' - requireRootInDeclaration: false - LambdaParameterNaming: - active: true - parameterPattern: '[a-z][A-Za-z0-9]*|_' - MatchingDeclarationName: active: true - mustBeFirst: true - multiplatformTargets: - - 'ios' - - 'android' - - 'js' - - 'jvm' - - 'native' - - 'iosArm64' - - 'iosX64' - - 'macosX64' - - 'mingwX64' - - 'linuxX64' - MemberNameEqualsClassName: - active: true - ignoreOverridden: true - NoNameShadowing: - active: true - NonBooleanPropertyPrefixedWithIs: - active: true - ObjectPropertyNaming: - active: true - constantPattern: '[A-Za-z][_A-Za-z0-9]*' - propertyPattern: '[A-Za-z][_A-Za-z0-9]*' - privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' - PackageNaming: - active: true - packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*' - TopLevelPropertyNaming: - active: true - constantPattern: '[A-Z][_A-Z0-9]*' - propertyPattern: '[A-Za-z][_A-Za-z0-9]*' - privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*' - VariableMaxLength: - active: false - maximumVariableNameLength: 64 - VariableMinLength: - active: false - minimumVariableNameLength: 1 - VariableNaming: - active: true - variablePattern: '[a-z][A-Za-z0-9]*' - privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' - excludeClassPattern: '$^' + BooleanPropertyNaming: + active: true + allowedPattern: '^(is|has|are)' + ClassNaming: + active: true + classPattern: '[A-Z][a-zA-Z0-9]*' + ConstructorParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + privateParameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + EnumNaming: + active: true + enumEntryPattern: '[A-Z][_a-zA-Z0-9]*' + ForbiddenClassName: + active: false + forbiddenName: [ ] + FunctionMaxLength: + active: false + maximumFunctionNameLength: 30 + FunctionMinLength: + active: false + minimumFunctionNameLength: 3 + FunctionNaming: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**', '**/androidHostTest/**' ] + functionPattern: '[a-zA-Z][a-zA-Z0-9]*' + excludeClassPattern: '$^' + FunctionParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + InvalidPackageDeclaration: + active: true + rootPackage: '' + requireRootInDeclaration: false + LambdaParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*|_' + MatchingDeclarationName: + active: true + mustBeFirst: true + multiplatformTargets: + - 'ios' + - 'android' + - 'js' + - 'jvm' + - 'native' + - 'iosArm64' + - 'iosX64' + - 'macosX64' + - 'mingwX64' + - 'linuxX64' + MemberNameEqualsClassName: + active: true + ignoreOverridden: true + NoNameShadowing: + active: true + NonBooleanPropertyPrefixedWithIs: + active: true + ObjectPropertyNaming: + active: true + constantPattern: '[A-Za-z][_A-Za-z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' + PackageNaming: + active: true + packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*' + TopLevelPropertyNaming: + active: true + constantPattern: '[A-Z][_A-Z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*' + VariableMaxLength: + active: false + maximumVariableNameLength: 64 + VariableMinLength: + active: false + minimumVariableNameLength: 1 + VariableNaming: + active: true + variablePattern: '[a-z][A-Za-z0-9]*' + privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' performance: - active: true - ArrayPrimitive: - active: true - CouldBeSequence: - active: true - threshold: 3 - ForEachOnRange: - active: true - excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] - SpreadOperator: - active: true - excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] - UnnecessaryPartOfBinaryExpression: - active: true - UnnecessaryTemporaryInstantiation: active: true + ArrayPrimitive: + active: true + CouldBeSequence: + active: true + threshold: 3 + ForEachOnRange: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] + SpreadOperator: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] + UnnecessaryPartOfBinaryExpression: + active: true + UnnecessaryTemporaryInstantiation: + active: true potential-bugs: - active: true - AvoidReferentialEquality: - active: true - forbiddenTypePatterns: - - 'kotlin.String' - CastNullableToNonNullableType: - active: true - CastToNullableType: - active: true - Deprecation: - active: true - DontDowncastCollectionTypes: - active: true - DoubleMutabilityForCollection: - active: true - mutableTypes: - - 'kotlin.collections.MutableList' - - 'kotlin.collections.MutableMap' - - 'kotlin.collections.MutableSet' - - 'java.util.ArrayList' - - 'java.util.LinkedHashSet' - - 'java.util.HashSet' - - 'java.util.LinkedHashMap' - - 'java.util.HashMap' - ElseCaseInsteadOfExhaustiveWhen: - active: true - EqualsAlwaysReturnsTrueOrFalse: - active: true - EqualsWithHashCodeExist: - active: true - ExitOutsideMain: - active: true - ExplicitGarbageCollectionCall: - active: true - HasPlatformType: - active: true - IgnoredReturnValue: - active: true - restrictToConfig: true - returnValueAnnotations: - - 'CheckResult' - - '*.CheckResult' - - 'CheckReturnValue' - - '*.CheckReturnValue' - ignoreReturnValueAnnotations: - - 'CanIgnoreReturnValue' - - '*.CanIgnoreReturnValue' - returnValueTypes: - - 'kotlin.sequences.Sequence' - - 'kotlinx.coroutines.flow.*Flow' - - 'java.util.stream.*Stream' - ignoreFunctionCall: [ ] - ImplicitDefaultLocale: - active: true - ImplicitUnitReturnType: - active: true - allowExplicitReturnType: true - InvalidRange: - active: true - IteratorHasNextCallsNextMethod: - active: true - IteratorNotThrowingNoSuchElementException: - active: true - LateinitUsage: - active: false - excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] - ignoreOnClassesPattern: '' - MapGetWithNotNullAssertionOperator: - active: true - MissingPackageDeclaration: - active: true - excludes: [ '**/*.kts' ] - NullCheckOnMutableProperty: - active: true - NullableToStringCall: - active: true - PropertyUsedBeforeDeclaration: - active: true - UnconditionalJumpStatementInLoop: - active: true - UnnecessaryNotNullCheck: - active: true - UnnecessaryNotNullOperator: - active: true - UnnecessarySafeCall: - active: true - UnreachableCatchBlock: - active: true - UnreachableCode: - active: true - UnsafeCallOnNullableType: - active: true - excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] - UnsafeCast: - active: true - UnusedUnaryOperator: - active: true - UselessPostfixExpression: - active: true - WrongEqualsTypeParameter: active: true + AvoidReferentialEquality: + active: true + forbiddenTypePatterns: + - 'kotlin.String' + CastNullableToNonNullableType: + active: true + CastToNullableType: + active: true + Deprecation: + active: true + DontDowncastCollectionTypes: + active: true + DoubleMutabilityForCollection: + active: true + mutableTypes: + - 'kotlin.collections.MutableList' + - 'kotlin.collections.MutableMap' + - 'kotlin.collections.MutableSet' + - 'java.util.ArrayList' + - 'java.util.LinkedHashSet' + - 'java.util.HashSet' + - 'java.util.LinkedHashMap' + - 'java.util.HashMap' + ElseCaseInsteadOfExhaustiveWhen: + active: true + EqualsAlwaysReturnsTrueOrFalse: + active: true + EqualsWithHashCodeExist: + active: true + ExitOutsideMain: + active: true + ExplicitGarbageCollectionCall: + active: true + HasPlatformType: + active: true + IgnoredReturnValue: + active: true + restrictToConfig: true + returnValueAnnotations: + - 'CheckResult' + - '*.CheckResult' + - 'CheckReturnValue' + - '*.CheckReturnValue' + ignoreReturnValueAnnotations: + - 'CanIgnoreReturnValue' + - '*.CanIgnoreReturnValue' + returnValueTypes: + - 'kotlin.sequences.Sequence' + - 'kotlinx.coroutines.flow.*Flow' + - 'java.util.stream.*Stream' + ignoreFunctionCall: [ ] + ImplicitDefaultLocale: + active: true + ImplicitUnitReturnType: + active: true + allowExplicitReturnType: true + InvalidRange: + active: true + IteratorHasNextCallsNextMethod: + active: true + IteratorNotThrowingNoSuchElementException: + active: true + LateinitUsage: + active: false + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] + ignoreOnClassesPattern: '' + MapGetWithNotNullAssertionOperator: + active: true + MissingPackageDeclaration: + active: true + excludes: [ '**/*.kts' ] + NullCheckOnMutableProperty: + active: true + NullableToStringCall: + active: true + PropertyUsedBeforeDeclaration: + active: true + UnconditionalJumpStatementInLoop: + active: true + UnnecessaryNotNullCheck: + active: true + UnnecessaryNotNullOperator: + active: true + UnnecessarySafeCall: + active: true + UnreachableCatchBlock: + active: true + UnreachableCode: + active: true + UnsafeCallOnNullableType: + active: true + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ] + UnsafeCast: + active: true + UnusedUnaryOperator: + active: true + UselessPostfixExpression: + active: true + WrongEqualsTypeParameter: + active: true style: - active: true - AlsoCouldBeApply: - active: true - BracesOnIfStatements: - active: false - singleLine: 'never' - multiLine: 'always' - BracesOnWhenStatements: - active: true - singleLine: 'necessary' - multiLine: 'consistent' - CanBeNonNullable: - active: true - CascadingCallWrapping: - active: false - includeElvis: true - ClassOrdering: - active: true - CollapsibleIfStatements: - active: true - DataClassContainsFunctions: - active: false - conversionFunctionPrefix: - - 'to' - allowOperators: false - DataClassShouldBeImmutable: - active: true - DestructuringDeclarationWithTooManyEntries: - active: true - maxDestructuringEntries: 3 - DoubleNegativeLambda: - active: true - negativeFunctions: - - reason: 'Use `takeIf` instead.' - value: 'takeUnless' - - reason: 'Use `all` instead.' - value: 'none' - negativeFunctionNameParts: - - 'not' - - 'non' - EqualsNullCall: - active: true - EqualsOnSignatureLine: - active: true - ExplicitCollectionElementAccessMethod: - active: true - ExplicitItLambdaParameter: - active: true - ExpressionBodySyntax: - active: true - includeLineWrapping: true - excludes: [ '**/*Module.kt' ] - ForbiddenAnnotation: - active: true - annotations: - - reason: 'it is a java annotation. Use `Suppress` instead.' - value: 'java.lang.SuppressWarnings' - - reason: 'it is a java annotation. Use `kotlin.Deprecated` instead.' - value: 'java.lang.Deprecated' - - reason: 'it is a java annotation. Use `kotlin.annotation.MustBeDocumented` instead.' - value: 'java.lang.annotation.Documented' - - reason: 'it is a java annotation. Use `kotlin.annotation.Target` instead.' - value: 'java.lang.annotation.Target' - - reason: 'it is a java annotation. Use `kotlin.annotation.Retention` instead.' - value: 'java.lang.annotation.Retention' - - reason: 'it is a java annotation. Use `kotlin.annotation.Repeatable` instead.' - value: 'java.lang.annotation.Repeatable' - - reason: 'Kotlin does not support @Inherited annotation, see https://youtrack.jetbrains.com/issue/KT-22265' - value: 'java.lang.annotation.Inherited' - ForbiddenComment: - active: true - comments: - - reason: 'Forbidden FIXME todo marker in comment, please fix the problem.' - value: 'FIXME:' - - reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.' - value: 'STOPSHIP:' - - reason: 'TODO comment format should be: // TODO: $description' - value: '// TODO:' - allowedPatterns: '^// TODO: .+$' - ForbiddenImport: - active: false - imports: [ ] - forbiddenPatterns: '' - ForbiddenMethodCall: - active: true - methods: - - reason: 'print does not allow you to configure the output stream. Use a logger instead.' - value: 'kotlin.io.print' - - reason: 'println does not allow you to configure the output stream. Use a logger instead.' - value: 'kotlin.io.println' - ForbiddenSuppress: - active: false - rules: [ ] - ForbiddenVoid: - active: true - ignoreOverridden: false - ignoreUsageInGenerics: false - FunctionOnlyReturningConstant: - active: true - ignoreOverridableFunction: true - ignoreActualFunction: true - excludedFunctions: [ ] - LoopWithTooManyJumpStatements: - active: true - maxJumpCount: 1 - MagicNumber: - active: false - excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts' ] - ignoreNumbers: - - '-1' - - '0' - - '1' - - '2' - ignoreHashCodeFunction: true - ignorePropertyDeclaration: false - ignoreLocalVariableDeclaration: false - ignoreConstantDeclaration: true - ignoreCompanionObjectPropertyDeclaration: true - ignoreAnnotation: false - ignoreNamedArgument: true - ignoreEnums: false - ignoreRanges: false - ignoreExtensionFunctions: true - MandatoryBracesLoops: - active: false - MaxChainedCallsOnSameLine: - active: true - maxChainedCalls: 5 - MaxLineLength: - active: true - maxLineLength: 120 - excludePackageStatements: true - excludeImportStatements: true - excludeCommentStatements: false - excludeRawStrings: true - MayBeConst: - active: true - ModifierOrder: - active: true - MultilineLambdaItParameter: - active: true - MultilineRawStringIndentation: - active: true - indentSize: 4 - trimmingMethods: - - 'trimIndent' - - 'trimMargin' - NestedClassesVisibility: - active: true - NewLineAtEndOfFile: - active: true - NoTabs: - active: true - NullableBooleanCheck: - active: true - ObjectLiteralToLambda: - active: true - OptionalAbstractKeyword: - active: true - OptionalUnit: - active: true - PreferToOverPairSyntax: - active: true - ProtectedMemberInFinalClass: - active: true - RedundantExplicitType: - active: true - RedundantHigherOrderMapUsage: - active: true - RedundantVisibilityModifierRule: - active: true - ReturnCount: - active: true - max: 2 - excludedFunctions: - - 'equals' - excludeLabeled: false - excludeReturnFromLambda: true - excludeGuardClauses: true - SafeCast: - active: true - SerialVersionUIDInSerializableClass: - active: true - SpacingBetweenPackageAndImports: - active: true - StringShouldBeRawString: - active: true - maxEscapedCharacterCount: 2 - ignoredCharacters: [ ] - ThrowsCount: - active: true - max: 2 - excludeGuardClauses: false - TrailingWhitespace: - active: true - TrimMultilineRawString: - active: true - trimmingMethods: - - 'trimIndent' - - 'trimMargin' - UnderscoresInNumericLiterals: - active: false - acceptableLength: 4 - allowNonStandardGrouping: false - UnnecessaryAbstractClass: - active: true - excludes: [ '**Module**' ] - UnnecessaryAnnotationUseSiteTarget: - active: true - UnnecessaryApply: - active: true - UnnecessaryBackticks: - active: true - UnnecessaryBracesAroundTrailingLambda: - active: true - UnnecessaryFilter: - active: true - UnnecessaryInheritance: - active: true - UnnecessaryInnerClass: - active: true - UnnecessaryLet: - active: true - UnnecessaryParentheses: - active: true - allowForUnclearPrecedence: true - UntilInsteadOfRangeTo: - active: true - UnusedImports: - active: true - UnusedParameter: - active: true - allowedNames: 'ignored|expected' - UnusedPrivateClass: - active: true - UnusedPrivateMember: - active: true - allowedNames: '' - ignoreAnnotated: - - 'Preview' - UnusedPrivateProperty: - active: true - allowedNames: '_|ignored|expected|serialVersionUID' - UseAnyOrNoneInsteadOfFind: - active: true - UseArrayLiteralsInAnnotations: - active: true - UseCheckNotNull: - active: true - UseCheckOrError: - active: true - UseDataClass: - active: true - allowVars: false - UseEmptyCounterpart: - active: true - UseIfEmptyOrIfBlank: - active: true - UseIfInsteadOfWhen: - active: true - ignoreWhenContainingVariableDeclaration: false - UseIsNullOrEmpty: - active: true - UseLet: - active: true - UseOrEmpty: - active: true - UseRequire: - active: true - UseRequireNotNull: - active: true - UseSumOfInsteadOfFlatMapSize: - active: true - UselessCallOnNotNull: - active: true - UtilityClassWithPublicConstructor: - active: true - VarCouldBeVal: - active: true - ignoreLateinitVar: false - WildcardImport: active: true - excludeImports: - - 'java.util.*' + AlsoCouldBeApply: + active: true + BracesOnIfStatements: + active: false + singleLine: 'never' + multiLine: 'always' + BracesOnWhenStatements: + active: true + singleLine: 'necessary' + multiLine: 'consistent' + CanBeNonNullable: + active: true + CascadingCallWrapping: + active: false + includeElvis: true + ClassOrdering: + active: true + CollapsibleIfStatements: + active: true + DataClassContainsFunctions: + active: false + conversionFunctionPrefix: + - 'to' + allowOperators: false + DataClassShouldBeImmutable: + active: true + DestructuringDeclarationWithTooManyEntries: + active: true + maxDestructuringEntries: 3 + DoubleNegativeLambda: + active: true + negativeFunctions: + - reason: 'Use `takeIf` instead.' + value: 'takeUnless' + - reason: 'Use `all` instead.' + value: 'none' + negativeFunctionNameParts: + - 'not' + - 'non' + EqualsNullCall: + active: true + EqualsOnSignatureLine: + active: true + ExplicitCollectionElementAccessMethod: + active: true + ExplicitItLambdaParameter: + active: true + ExpressionBodySyntax: + active: true + includeLineWrapping: true + excludes: [ '**/*Module.kt' ] + ForbiddenAnnotation: + active: true + annotations: + - reason: 'it is a java annotation. Use `Suppress` instead.' + value: 'java.lang.SuppressWarnings' + - reason: 'it is a java annotation. Use `kotlin.Deprecated` instead.' + value: 'java.lang.Deprecated' + - reason: 'it is a java annotation. Use `kotlin.annotation.MustBeDocumented` instead.' + value: 'java.lang.annotation.Documented' + - reason: 'it is a java annotation. Use `kotlin.annotation.Target` instead.' + value: 'java.lang.annotation.Target' + - reason: 'it is a java annotation. Use `kotlin.annotation.Retention` instead.' + value: 'java.lang.annotation.Retention' + - reason: 'it is a java annotation. Use `kotlin.annotation.Repeatable` instead.' + value: 'java.lang.annotation.Repeatable' + - reason: 'Kotlin does not support @Inherited annotation, see https://youtrack.jetbrains.com/issue/KT-22265' + value: 'java.lang.annotation.Inherited' + ForbiddenComment: + active: true + comments: + - reason: 'Forbidden FIXME todo marker in comment, please fix the problem.' + value: 'FIXME:' + - reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.' + value: 'STOPSHIP:' + - reason: 'TODO comment format should be: // TODO: $description' + value: '// TODO:' + allowedPatterns: '^// TODO: .+$' + ForbiddenImport: + active: false + imports: [ ] + forbiddenPatterns: '' + ForbiddenMethodCall: + active: true + methods: + - reason: 'print does not allow you to configure the output stream. Use a logger instead.' + value: 'kotlin.io.print' + - reason: 'println does not allow you to configure the output stream. Use a logger instead.' + value: 'kotlin.io.println' + ForbiddenSuppress: + active: false + rules: [ ] + ForbiddenVoid: + active: true + ignoreOverridden: false + ignoreUsageInGenerics: false + FunctionOnlyReturningConstant: + active: true + ignoreOverridableFunction: true + ignoreActualFunction: true + excludedFunctions: [ ] + LoopWithTooManyJumpStatements: + active: true + maxJumpCount: 1 + MagicNumber: + active: false + excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts' ] + ignoreNumbers: + - '-1' + - '0' + - '1' + - '2' + ignoreHashCodeFunction: true + ignorePropertyDeclaration: false + ignoreLocalVariableDeclaration: false + ignoreConstantDeclaration: true + ignoreCompanionObjectPropertyDeclaration: true + ignoreAnnotation: false + ignoreNamedArgument: true + ignoreEnums: false + ignoreRanges: false + ignoreExtensionFunctions: true + MandatoryBracesLoops: + active: false + MaxChainedCallsOnSameLine: + active: true + maxChainedCalls: 5 + MaxLineLength: + active: true + maxLineLength: 120 + excludePackageStatements: true + excludeImportStatements: true + excludeCommentStatements: false + excludeRawStrings: true + MayBeConst: + active: true + ModifierOrder: + active: true + MultilineLambdaItParameter: + active: true + MultilineRawStringIndentation: + active: true + indentSize: 4 + trimmingMethods: + - 'trimIndent' + - 'trimMargin' + NestedClassesVisibility: + active: true + NewLineAtEndOfFile: + active: true + NoTabs: + active: true + NullableBooleanCheck: + active: true + ObjectLiteralToLambda: + active: true + OptionalAbstractKeyword: + active: true + OptionalUnit: + active: true + PreferToOverPairSyntax: + active: true + ProtectedMemberInFinalClass: + active: true + RedundantExplicitType: + active: true + RedundantHigherOrderMapUsage: + active: true + RedundantVisibilityModifierRule: + active: true + ReturnCount: + active: true + max: 2 + excludedFunctions: + - 'equals' + excludeLabeled: false + excludeReturnFromLambda: true + excludeGuardClauses: true + SafeCast: + active: true + SerialVersionUIDInSerializableClass: + active: true + SpacingBetweenPackageAndImports: + active: true + StringShouldBeRawString: + active: true + maxEscapedCharacterCount: 2 + ignoredCharacters: [ ] + ThrowsCount: + active: true + max: 2 + excludeGuardClauses: false + TrailingWhitespace: + active: true + TrimMultilineRawString: + active: true + trimmingMethods: + - 'trimIndent' + - 'trimMargin' + UnderscoresInNumericLiterals: + active: false + acceptableLength: 4 + allowNonStandardGrouping: false + UnnecessaryAbstractClass: + active: true + excludes: [ '**Module**' ] + UnnecessaryAnnotationUseSiteTarget: + active: true + UnnecessaryApply: + active: true + UnnecessaryBackticks: + active: true + UnnecessaryBracesAroundTrailingLambda: + active: true + UnnecessaryFilter: + active: true + UnnecessaryInheritance: + active: true + UnnecessaryInnerClass: + active: true + UnnecessaryLet: + active: true + UnnecessaryParentheses: + active: true + allowForUnclearPrecedence: true + UntilInsteadOfRangeTo: + active: true + UnusedImports: + active: true + UnusedParameter: + active: true + allowedNames: 'ignored|expected' + UnusedPrivateClass: + active: true + UnusedPrivateMember: + active: true + allowedNames: '' + ignoreAnnotated: + - 'Preview' + UnusedPrivateProperty: + active: true + allowedNames: '_|ignored|expected|serialVersionUID' + UseAnyOrNoneInsteadOfFind: + active: true + UseArrayLiteralsInAnnotations: + active: true + UseCheckNotNull: + active: true + UseCheckOrError: + active: true + UseDataClass: + active: true + allowVars: false + UseEmptyCounterpart: + active: true + UseIfEmptyOrIfBlank: + active: true + UseIfInsteadOfWhen: + active: true + ignoreWhenContainingVariableDeclaration: false + UseIsNullOrEmpty: + active: true + UseLet: + active: true + UseOrEmpty: + active: true + UseRequire: + active: true + UseRequireNotNull: + active: true + UseSumOfInsteadOfFlatMapSize: + active: true + UselessCallOnNotNull: + active: true + UtilityClassWithPublicConstructor: + active: true + VarCouldBeVal: + active: true + ignoreLateinitVar: false + WildcardImport: + active: true + excludeImports: + - 'java.util.*' From 07f56b15bdb723929dc33900fba10d70f9fdc1a1 Mon Sep 17 00:00:00 2001 From: andreia Date: Wed, 27 May 2026 10:46:01 +0200 Subject: [PATCH 15/15] simplify TaskValueMapper structure --- .../android/di/GroundApplicationModule.kt | 11 ---- .../ui/mapper/TaskValueMapper.kt | 37 +++++++------- .../ui/mapper/TaskValueMapperTest.kt | 50 ++++++++++--------- 3 files changed, 43 insertions(+), 55 deletions(-) diff --git a/app/src/main/java/org/groundplatform/android/di/GroundApplicationModule.kt b/app/src/main/java/org/groundplatform/android/di/GroundApplicationModule.kt index 6ecfed8340..220304232a 100644 --- a/app/src/main/java/org/groundplatform/android/di/GroundApplicationModule.kt +++ b/app/src/main/java/org/groundplatform/android/di/GroundApplicationModule.kt @@ -27,10 +27,6 @@ import java.util.Locale import javax.inject.Singleton import org.groundplatform.android.R import org.groundplatform.android.util.SurveyDeepLinkParser -import org.groundplatform.ui.util.AndroidDateFormatter -import org.groundplatform.ui.util.ComposeStringResolver -import org.groundplatform.ui.util.DateFormatter -import org.groundplatform.ui.util.StringResolver @InstallIn(SingletonComponent::class) @Module(includes = [ViewModelModule::class]) @@ -51,11 +47,4 @@ object GroundApplicationModule { deepLinkHost = resources.getString(R.string.deeplink_host), deepLinkPath = resources.getString(R.string.survey_deeplink_path), ) - - @Provides - @Singleton - fun provideDateFormatter(@ApplicationContext context: Context): DateFormatter = - AndroidDateFormatter(context) - - @Provides @Singleton fun provideStringResolver(): StringResolver = ComposeStringResolver } diff --git a/core/ui/src/commonMain/kotlin/org/groundplatform/ui/mapper/TaskValueMapper.kt b/core/ui/src/commonMain/kotlin/org/groundplatform/ui/mapper/TaskValueMapper.kt index 3eb7447e38..121886bb8f 100644 --- a/core/ui/src/commonMain/kotlin/org/groundplatform/ui/mapper/TaskValueMapper.kt +++ b/core/ui/src/commonMain/kotlin/org/groundplatform/ui/mapper/TaskValueMapper.kt @@ -42,29 +42,26 @@ import org.groundplatform.ui.model.SubmissionPdfDocument.Answer import org.groundplatform.ui.util.DateFormatter import org.groundplatform.ui.util.StringResolver -object TaskValueMapper { - private const val DEGREES_DECIMALS = 6 +class TaskValueMapper( + private val strings: StringResolver, + private val dateFormatter: DateFormatter, +) { - /** Maps a [TaskData] value to the [Answer] to be rendered in the submission PDF. */ - suspend fun map( - task: Task, - value: TaskData, - dateFormatter: DateFormatter, - strings: StringResolver, - ): Answer = + /** Maps [Task] and [TaskData] values to the [Answer] to be rendered in the submission PDF. */ + suspend fun map(task: Task, value: TaskData): Answer = when (value) { is SkippedTaskData -> Answer.Text(listOf(strings.resolve(Res.string.skipped))) is TextTaskData -> Answer.Text(listOf(value.text)) is NumberTaskData -> Answer.Text(listOf(value.number)) is DateTimeTaskData -> - Answer.Text(listOfNotNull(formatTaskDateTime(task, value.timeInMillis, dateFormatter))) - is MultipleChoiceTaskData -> Answer.Text(formatMultipleChoice(task, value, strings)) - is CaptureLocationTaskData -> Answer.Text(formatCaptureLocation(value, strings)) + Answer.Text(listOfNotNull(formatTaskDateTime(task, value.timeInMillis))) + is MultipleChoiceTaskData -> Answer.Text(formatMultipleChoice(task, value)) + is CaptureLocationTaskData -> Answer.Text(formatCaptureLocation(value)) is PhotoTaskData -> Answer.Photo(value.remoteFilename) else -> Answer.Text(emptyList()) } - private fun formatTaskDateTime(task: Task, millis: Long, dateFormatter: DateFormatter): String? = + private fun formatTaskDateTime(task: Task, millis: Long): String? = when (task.type) { Task.Type.DATE -> dateFormatter.formatDate(millis) Task.Type.TIME -> dateFormatter.formatTime(millis) @@ -74,7 +71,6 @@ object TaskValueMapper { private suspend fun formatMultipleChoice( task: Task, value: MultipleChoiceTaskData, - strings: StringResolver, ): List { val options = task.multipleChoice?.options.orEmpty() val selectedLabels = @@ -89,17 +85,14 @@ object TaskValueMapper { } /** Coordinates first, then optional altitude and accuracy lines. */ - private suspend fun formatCaptureLocation( - value: CaptureLocationTaskData, - strings: StringResolver, - ): List { - val lines = mutableListOf(formatPoint(value.location, strings)) + private suspend fun formatCaptureLocation(value: CaptureLocationTaskData): List { + val lines = mutableListOf(formatPoint(value.location)) value.altitude?.let { lines.add(strings.resolve(Res.string.pdf_altitude, formatMeters(it))) } value.accuracy?.let { lines.add(strings.resolve(Res.string.pdf_accuracy, formatMeters(it))) } return lines } - private suspend fun formatPoint(point: Point, strings: StringResolver): String { + private suspend fun formatPoint(point: Point): String { val lat = point.coordinates.lat val lng = point.coordinates.lng val latDir = @@ -114,4 +107,8 @@ object TaskValueMapper { "${value.absoluteValue.toFixedDecimals(DEGREES_DECIMALS)}°" @VisibleForTesting fun formatMeters(value: Double): String = round(value).toLong().toString() + + companion object { + private const val DEGREES_DECIMALS = 6 + } } diff --git a/core/ui/src/commonTest/kotlin/org/groundplatform/ui/mapper/TaskValueMapperTest.kt b/core/ui/src/commonTest/kotlin/org/groundplatform/ui/mapper/TaskValueMapperTest.kt index 3fe80a949c..04b24f772d 100644 --- a/core/ui/src/commonTest/kotlin/org/groundplatform/ui/mapper/TaskValueMapperTest.kt +++ b/core/ui/src/commonTest/kotlin/org/groundplatform/ui/mapper/TaskValueMapperTest.kt @@ -27,7 +27,6 @@ import org.groundplatform.domain.model.submission.DropPinTaskData import org.groundplatform.domain.model.submission.MultipleChoiceTaskData import org.groundplatform.domain.model.submission.NumberTaskData import org.groundplatform.domain.model.submission.SkippedTaskData -import org.groundplatform.domain.model.submission.TaskData import org.groundplatform.domain.model.submission.TextTaskData import org.groundplatform.domain.model.task.MultipleChoice import org.groundplatform.domain.model.task.Option @@ -42,37 +41,44 @@ class TaskValueMapperTest { private val dateFormatter = FakeDateFormatter private val stringResolver = FakeStringResolver + private val mapper = TaskValueMapper(strings = stringResolver, dateFormatter = dateFormatter) @Test fun `TEXT task maps to the same value`() = runTest { assertEquals( Answer.Text(listOf("free text")), - map(task(Task.Type.TEXT), TextTaskData("free text")), + mapper.map(task(Task.Type.TEXT), TextTaskData("free text")), ) } @Test fun `NUMBER task maps to the same value`() = runTest { - assertEquals(Answer.Text(listOf("42")), map(task(Task.Type.NUMBER), NumberTaskData("42"))) + assertEquals( + Answer.Text(listOf("42")), + mapper.map(task(Task.Type.NUMBER), NumberTaskData("42")), + ) } @Test fun `Skipped value renders the skipped label`() = runTest { - assertEquals(Answer.Text(listOf("skipped")), map(task(Task.Type.TEXT), SkippedTaskData())) + assertEquals( + Answer.Text(listOf("skipped")), + mapper.map(task(Task.Type.TEXT), SkippedTaskData()), + ) } @Test fun `PHOTO task maps to a photo answer`() = runTest { assertEquals( Answer.Photo("path/to/photo.jpg"), - map(task(Task.Type.PHOTO), PhotoTaskData("path/to/photo.jpg")), + mapper.map(task(Task.Type.PHOTO), PhotoTaskData("path/to/photo.jpg")), ) } @Test fun `unsupported value maps to an empty answer`() = runTest { val value = DropPinTaskData(Point(Coordinates(1.0, 2.0))) - assertEquals(Answer.Text(emptyList()), map(task(Task.Type.DROP_PIN), value)) + assertEquals(Answer.Text(emptyList()), mapper.map(task(Task.Type.DROP_PIN), value)) } @Test @@ -80,7 +86,7 @@ class TaskValueMapperTest { val millis = 987654321L assertEquals( Answer.Text(listOf(dateFormatter.formatDate(millis))), - map(task(Task.Type.DATE), DateTimeTaskData(millis)), + mapper.map(task(Task.Type.DATE), DateTimeTaskData(millis)), ) } @@ -89,14 +95,17 @@ class TaskValueMapperTest { val millis = 987654321L assertEquals( Answer.Text(listOf(dateFormatter.formatTime(millis))), - map(task(Task.Type.TIME), DateTimeTaskData(millis)), + mapper.map(task(Task.Type.TIME), DateTimeTaskData(millis)), ) } @Test fun `non date or time task renders empty answer`() = runTest { val millis = 987654321L - assertEquals(Answer.Text(emptyList()), map(task(Task.Type.NUMBER), DateTimeTaskData(millis))) + assertEquals( + Answer.Text(emptyList()), + mapper.map(task(Task.Type.NUMBER), DateTimeTaskData(millis)), + ) } @Test @@ -104,7 +113,7 @@ class TaskValueMapperTest { val value = MultipleChoiceTaskData(multipleChoice(), selectedOptionIds = listOf("a", "b")) assertEquals( Answer.Text(listOf("Apple", "Banana")), - map(task(Task.Type.MULTIPLE_CHOICE, multipleChoice()), value), + mapper.map(task(Task.Type.MULTIPLE_CHOICE, multipleChoice()), value), ) } @@ -114,7 +123,7 @@ class TaskValueMapperTest { val value = MultipleChoiceTaskData(multipleChoice(), selectedOptionIds = listOf("a", other)) assertEquals( Answer.Text(listOf("Apple", "other: custom")), - map(task(Task.Type.MULTIPLE_CHOICE, multipleChoice()), value), + mapper.map(task(Task.Type.MULTIPLE_CHOICE, multipleChoice()), value), ) } @@ -123,14 +132,14 @@ class TaskValueMapperTest { val value = CaptureLocationTaskData(Point(Coordinates(1.5, -2.25)), altitude = 10.0, accuracy = 3.0) - val result = map(task(Task.Type.CAPTURE_LOCATION), value) + val result = mapper.map(task(Task.Type.CAPTURE_LOCATION), value) val expected = Answer.Text( listOf( - "${TaskValueMapper.formatDegrees(1.5)} north, ${TaskValueMapper.formatDegrees(2.25)} west", - "pdf_altitude(${TaskValueMapper.formatMeters(10.0)})", - "pdf_accuracy(${TaskValueMapper.formatMeters(3.0)})", + "${mapper.formatDegrees(1.5)} north, ${mapper.formatDegrees(2.25)} west", + "pdf_altitude(${mapper.formatMeters(10.0)})", + "pdf_accuracy(${mapper.formatMeters(3.0)})", ) ) assertEquals(expected, result) @@ -141,21 +150,14 @@ class TaskValueMapperTest { val value = CaptureLocationTaskData(Point(Coordinates(-1.0, 2.0)), altitude = null, accuracy = null) - val result = map(task(Task.Type.CAPTURE_LOCATION), value) + val result = mapper.map(task(Task.Type.CAPTURE_LOCATION), value) assertEquals( - Answer.Text( - listOf( - "${TaskValueMapper.formatDegrees(1.0)} south, ${TaskValueMapper.formatDegrees(2.0)} east" - ) - ), + Answer.Text(listOf("${mapper.formatDegrees(1.0)} south, ${mapper.formatDegrees(2.0)} east")), result, ) } - private suspend fun map(task: Task, value: TaskData) = - TaskValueMapper.map(task, value, dateFormatter, stringResolver) - private fun task(type: Task.Type, multipleChoice: MultipleChoice? = null) = FakeDataGenerator.newTask(type = type, multipleChoice = multipleChoice)