Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
660f653
add new strings for pdf labels
andreia-ferreira May 28, 2026
3b8cb9e
add entry point for pdf export PdfExportService and the interfaces th…
andreia-ferreira May 28, 2026
b061d06
update LoiReport and add mapper to map the data to the PdfExportServi…
andreia-ferreira May 28, 2026
0aa877e
remove unneeded code from gradle and move ComposeStringResourcesTest …
andreia-ferreira May 28, 2026
414a2d3
fix codecheck issues
andreia-ferreira May 28, 2026
858a7c3
extract pdf code to sepparate feature module
andreia-ferreira May 29, 2026
86e7780
Merge branch 'master' into andreia/3754/pdf-renderers
andreia-ferreira May 29, 2026
30ccb64
move testing helpers to feature:pdf
andreia-ferreira May 29, 2026
f890701
fix failing tests
andreia-ferreira May 29, 2026
36a8524
add missing file license
andreia-ferreira May 29, 2026
84c60de
fix test
andreia-ferreira May 29, 2026
4fac1a0
handle cases where rendering might fail and create a corrupt file
andreia-ferreira Jun 1, 2026
89c647b
fix file names not handling all types of characters
andreia-ferreira Jun 1, 2026
c686435
Merge branch 'master' into andreia/3754/pdf-renderers
andreia-ferreira Jun 1, 2026
928e802
add common layout components to render
andreia-ferreira Jun 2, 2026
f204be0
update QrCodeGenerator to provide bitmap+logo for PDF documents
andreia-ferreira Jun 2, 2026
6a4f8a7
add android implementations for PDF platform interfaces
andreia-ferreira Jun 2, 2026
49db9bc
add unit tests for PdfCursor and PdfPageController
andreia-ferreira Jun 2, 2026
bec48ec
add LoiReportAction to propagate the interactions with SubmissionPdfI…
andreia-ferreira Jun 2, 2026
ad8d18c
add getSubmissions to the SubmissionRepository and apply it to setup …
andreia-ferreira Jun 2, 2026
7cfe3e1
update SubmissionData to not treat skipped and null submissions equally
andreia-ferreira Jun 3, 2026
1bb98f2
extract common logic for the footer reserve and table building
andreia-ferreira Jun 3, 2026
255f1e2
extract fragment logic to LoiExporter and add tests
andreia-ferreira Jun 8, 2026
64a223c
Merge remote-tracking branch 'origin/master' into andreia/3754/pdf-re…
andreia-ferreira Jun 8, 2026
54e67b3
fix merge conflicts
andreia-ferreira Jun 9, 2026
9fa281f
Merge remote-tracking branch 'origin/master' into andreia/3739/pdf-re…
andreia-ferreira Jun 9, 2026
ee00650
add common layout components to render
andreia-ferreira Jun 2, 2026
7b27c49
update QrCodeGenerator to provide bitmap+logo for PDF documents
andreia-ferreira Jun 2, 2026
87d51a4
add android implementations for PDF platform interfaces
andreia-ferreira Jun 2, 2026
dcbca6f
add unit tests for PdfCursor and PdfPageController
andreia-ferreira Jun 2, 2026
5a163df
extract common logic for the footer reserve and table building
andreia-ferreira Jun 3, 2026
90a1d81
fix code formatting
andreia-ferreira Jun 9, 2026
b4b1fa8
improve test coverage
andreia-ferreira Jun 9, 2026
3772ba2
add tests for PdfWriter pagination
andreia-ferreira Jun 9, 2026
0eb9444
add logging for AndroidPdfImageProvider
andreia-ferreira Jun 9, 2026
d7bd17c
add todo comment about iOS support for the implementations
andreia-ferreira Jun 9, 2026
4762a2a
simplify AndroidPdfImageProvider
andreia-ferreira Jun 9, 2026
6588980
Merge branch 'andreia/3739/pdf-report-layout' into andreia/3739/pdf-r…
andreia-ferreira Jun 9, 2026
5930d55
Merge branch 'master' into andreia/3739/pdf-report-layout
shobhitagarwal1612 Jun 10, 2026
06e0b4e
add unit tests for PdfWriter; AndroidPdfOutputProvider; PdfImageSet a…
andreia-ferreira Jun 10, 2026
86bab5b
add unit tests for PdfWriter; AndroidPdfOutputProvider; PdfImageSet a…
andreia-ferreira Jun 10, 2026
8d5137b
fix code style check
andreia-ferreira Jun 10, 2026
b7d18a6
improve AndroidPdfImageProviderTest; add tests for DocumentPdfCanvas …
andreia-ferreira Jun 10, 2026
ef99661
Merge branch 'master' into andreia/3739/pdf-report-layout
andreia-ferreira Jun 11, 2026
a7d119e
update AndroidPdfImageProvider#load to use async/awaitAll
andreia-ferreira Jun 11, 2026
14fc959
simplify calculateInSampleSize
andreia-ferreira Jun 11, 2026
2f7b3ba
apply suggestion to PdfWriter
andreia-ferreira Jun 11, 2026
d388ce7
add IO dispatcher to AndroidPdfRenderer file operation
andreia-ferreira Jun 11, 2026
8948a19
update top border drawing logic for the table
andreia-ferreira Jun 11, 2026
25ffb1f
add unit tests to assure PdfWriter only draws one internal border bet…
andreia-ferreira Jun 11, 2026
2514a5d
Merge branch 'andreia/3739/pdf-report-layout' into andreia/3739/pdf-r…
andreia-ferreira Jun 12, 2026
9523ba2
update di setup with correct coroutines
andreia-ferreira Jun 16, 2026
7165323
fix incomplete tests
andreia-ferreira Jun 16, 2026
86a7f1e
move LoiReportExporter call to DataCollectionViewModel
andreia-ferreira Jun 16, 2026
b85f4f9
add HomeScreenMapContainerUiEffect for the VM effects
andreia-ferreira Jun 16, 2026
2f04c13
move LoiReportExporter to HomeScreenMapContainerViewModel and update …
andreia-ferreira Jun 16, 2026
2a04d81
add unit tests for data collection and home screen
andreia-ferreira Jun 17, 2026
77e1f86
update composable tests
andreia-ferreira Jun 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation project(':core:domain')
implementation project(':core:ui')
implementation project(':feature:pdf')
implementation libs.androidx.multidex
implementation libs.androidx.preference.ktx

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand All @@ -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
}
112 changes: 112 additions & 0 deletions app/src/main/java/org/groundplatform/android/di/PdfModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* 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.android.di

import android.content.Context
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineDispatcher
import org.groundplatform.android.BuildConfig
import org.groundplatform.android.R
import org.groundplatform.android.di.coroutines.DefaultDispatcher
import org.groundplatform.android.di.coroutines.IoDispatcher
import org.groundplatform.feature.pdf.AndroidPdfImageProvider
import org.groundplatform.feature.pdf.AndroidPdfOutputProvider
import org.groundplatform.feature.pdf.AndroidPdfRenderer
import org.groundplatform.feature.pdf.AndroidPdfReportLauncher
import org.groundplatform.feature.pdf.LoiReportExporter
import org.groundplatform.feature.pdf.PdfExportService
import org.groundplatform.feature.pdf.PdfImageProvider
import org.groundplatform.feature.pdf.PdfOutputProvider
import org.groundplatform.feature.pdf.PdfRenderer
import org.groundplatform.feature.pdf.PdfReportLauncher
import org.groundplatform.feature.pdf.mapper.LoiReportMapper
import org.groundplatform.feature.pdf.mapper.TaskValueMapper
import org.groundplatform.ui.util.DateFormatter
import org.groundplatform.ui.util.StringResolver

@Module
@InstallIn(SingletonComponent::class)
object PdfModule {

@Provides
@Singleton
fun providePdfImageProvider(@ApplicationContext context: Context): PdfImageProvider =
AndroidPdfImageProvider(context = context, logoDrawableRes = R.drawable.ground_logo)

@Provides
@Singleton
fun providePdfOutputFactory(@ApplicationContext context: Context): PdfOutputProvider =
AndroidPdfOutputProvider(context)

@Provides
@Singleton
fun providePdfRenderer(@IoDispatcher ioDispatcher: CoroutineDispatcher): PdfRenderer =
AndroidPdfRenderer(ioDispatcher)

@Provides
@Singleton
fun provideTaskValueMapper(
strings: StringResolver,
dateFormatter: DateFormatter,
): TaskValueMapper = TaskValueMapper(strings = strings, dateFormatter = dateFormatter)

@Provides
@Singleton
fun provideLoiReportMapper(
taskValueMapper: TaskValueMapper,
strings: StringResolver,
dateFormatter: DateFormatter,
): LoiReportMapper =
LoiReportMapper(
taskValueMapper = taskValueMapper,
strings = strings,
dateFormatter = dateFormatter,
)

@Provides
@Singleton
fun providePdfReportLauncher(@ApplicationContext context: Context): PdfReportLauncher =
AndroidPdfReportLauncher(context = context, fileProviderAuthority = BuildConfig.APPLICATION_ID)

@Provides
@Singleton
fun providePdfReportService(
imageProvider: PdfImageProvider,
renderer: PdfRenderer,
outputProvider: PdfOutputProvider,
launcher: PdfReportLauncher,
@DefaultDispatcher coroutineDispatcher: CoroutineDispatcher,
): PdfExportService =
PdfExportService(
imageProvider = imageProvider,
renderer = renderer,
outputProvider = outputProvider,
launcher = launcher,
coroutineDispatcher = coroutineDispatcher,
)

@Provides
@Singleton
fun provideLoiReportExporter(
mapper: LoiReportMapper,
exportService: PdfExportService,
): LoiReportExporter = LoiReportExporter(mapper = mapper, exportService = exportService)
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,14 @@ object UseCaseModule {
locationOfInterestRepository: LocationOfInterestRepositoryInterface,
userRepository: UserRepositoryInterface,
surveyRepository: SurveyRepositoryInterface,
) = GetLoiReportUseCase(locationOfInterestRepository, userRepository, surveyRepository)
submissionRepository: SubmissionRepositoryInterface,
) =
GetLoiReportUseCase(
locationOfInterestRepository = locationOfInterestRepository,
userRepositoryInterface = userRepository,
surveyRepositoryInterface = surveyRepository,
submissionRepositoryInterface = submissionRepository,
)

@Provides
fun providesUpdateUserSettingsUseCase(userRepository: UserRepositoryInterface) =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,14 @@ object CoroutineDispatchersModule {
@IoDispatcher @Provides fun provideIoDispatcher(): CoroutineDispatcher = Dispatchers.IO

@MainDispatcher @Provides fun provideMainDispatcher(): CoroutineDispatcher = Dispatchers.Main

@DefaultDispatcher
@Provides
fun provideDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
}

@Retention(AnnotationRetention.RUNTIME) @Qualifier annotation class DefaultDispatcher

@Retention(AnnotationRetention.RUNTIME) @Qualifier annotation class IoDispatcher

@Retention(AnnotationRetention.RUNTIME) @Qualifier annotation class MainDispatcher
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,7 @@ constructor(

private suspend fun getPendingDeleteCount(loiId: String) =
localSubmissionStore.getPendingDeleteCount(loiId)

override suspend fun getSubmissions(loi: LocationOfInterest) =
localSubmissionStore.getSubmissions(loi, loi.job.id)
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ import android.view.ViewGroup
import androidx.hilt.navigation.fragment.hiltNavGraphViewModels
import androidx.navigation.fragment.findNavController
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.groundplatform.android.R
import org.groundplatform.android.ui.common.AbstractFragment
import org.groundplatform.android.ui.common.BackPressListener
import org.groundplatform.android.ui.common.EphemeralPopups
import org.groundplatform.android.ui.home.HomeScreenViewModel
import org.groundplatform.android.util.createComposeView
import org.groundplatform.android.util.openAppSettings
import javax.inject.Inject

/** Fragment allowing the user to collect data to complete a task. */
@AndroidEntryPoint
Expand All @@ -55,6 +55,7 @@ class DataCollectionFragment : AbstractFragment(), BackPressListener {
onExitConfirmed = { navigateBack() },
onOpenSettings = { requireActivity().openAppSettings() },
onAwaitingPhotoCapture = { homeScreenViewModel.awaitingPhotoCapture = it },
onReportExportError = { popups.ErrorPopup().unknownError() },
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.groundplatform.android.R
import org.groundplatform.android.ui.components.ConfirmationDialog
import org.groundplatform.android.ui.datacollection.tasks.TaskScreenContainer
import org.groundplatform.ui.components.loireport.LoiReportAction

/**
* The main screen for data collection, coordinating the task sequence and host UI.
Expand All @@ -57,6 +58,7 @@ import org.groundplatform.android.ui.datacollection.tasks.TaskScreenContainer
fun DataCollectionScreen(
viewModel: DataCollectionViewModel,
onValidationError: (resId: Int) -> Unit,
onReportExportError: () -> Unit,
onExitConfirmed: () -> Unit,
onOpenSettings: () -> Unit,
onAwaitingPhotoCapture: (Boolean) -> Unit,
Expand All @@ -71,11 +73,16 @@ fun DataCollectionScreen(
is DataCollectionUiEffect.OpenSettings -> onOpenSettings()
is DataCollectionUiEffect.SetAwaitingPhotoCapture -> onAwaitingPhotoCapture(effect.awaiting)
is DataCollectionUiEffect.ShowValidationError -> onValidationError(effect.errorResId)
is DataCollectionUiEffect.ShowReportExportError -> onReportExportError()
}
}
}

DataCollectionContent(uiState = uiState, onCloseClicked = { viewModel.onCloseClicked() }) {
DataCollectionContent(
uiState = uiState,
onCloseClicked = { viewModel.onCloseClicked() },
onLoiReportAction = { viewModel.onLoiReportAction(it) },
) {
readyState ->
val tasks = readyState.tasks
if (tasks.isNotEmpty()) {
Expand Down Expand Up @@ -134,6 +141,7 @@ object DataCollectionScreenTestTags {
fun DataCollectionContent(
uiState: DataCollectionUiState,
onCloseClicked: () -> Unit,
onLoiReportAction: (LoiReportAction) -> Unit,
pagerContent: @Composable (DataCollectionUiState.Ready) -> Unit,
) {
Scaffold(topBar = { DataCollectionToolbar(uiState, onCloseClicked) }) { innerPadding ->
Expand All @@ -153,7 +161,11 @@ fun DataCollectionContent(
ReadyContent { pagerContent(uiState) }
}
is DataCollectionUiState.TaskSubmitted -> {
DataSubmissionConfirmationScreen(loiReport = uiState.loiReport) { onCloseClicked() }
DataSubmissionConfirmationScreen(
loiReport = uiState.loiReport,
onLoiReportAction = onLoiReportAction,
onDismissed = onCloseClicked,
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import org.groundplatform.android.ui.common.ExcludeFromJacocoGeneratedReport
import org.groundplatform.domain.model.job.Job
import org.groundplatform.domain.model.locationofinterest.LoiReport
import org.groundplatform.ui.theme.AppTheme
import kotlin.time.Clock

private const val PAGER_CONTENT_TEXT = "Pager Content Area"

Expand All @@ -37,7 +38,11 @@ private const val PAGER_CONTENT_TEXT = "Pager Content Area"
@ExcludeFromJacocoGeneratedReport
private fun DataCollectionContentLoadingPreview() {
AppTheme {
DataCollectionContent(uiState = DataCollectionUiState.Loading, onCloseClicked = {}) {
DataCollectionContent(
uiState = DataCollectionUiState.Loading,
onCloseClicked = {},
onLoiReportAction = {},
) {
Box(modifier = Modifier.fillMaxSize().background(Color.LightGray)) {
Text(text = PAGER_CONTENT_TEXT, modifier = Modifier.align(Alignment.Center))
}
Expand All @@ -57,6 +62,7 @@ private fun DataCollectionContentErrorPreview() {
cause = Error("Some error"),
),
onCloseClicked = {},
onLoiReportAction = {},
) {
Box(modifier = Modifier.fillMaxSize().background(Color.LightGray)) {
Text(text = PAGER_CONTENT_TEXT, modifier = Modifier.align(Alignment.Center))
Expand All @@ -82,6 +88,7 @@ private fun DataCollectionContentPreview() {
position = TaskPosition(0, 1, 3),
),
onCloseClicked = {},
onLoiReportAction = {},
) {
Box(modifier = Modifier.fillMaxSize().background(Color.LightGray)) {
Text(text = PAGER_CONTENT_TEXT, modifier = Modifier.align(Alignment.Center))
Expand All @@ -99,9 +106,21 @@ private fun DataCollectionContentCompletePreview() {
uiState =
DataCollectionUiState.TaskSubmitted(
loiReport =
LoiReport(loiName = "Point A", geoJson = JsonObject(mapOf()), submissionDetails = null)
LoiReport(
loiName = "Point A",
geoJson = JsonObject(mapOf()),
submissionDetails =
LoiReport.SubmissionDetails(
surveyName = "Test Survey",
userName = "John Doe",
userEmail = "john.doe@example.com",
dateMillis = Clock.System.now().toEpochMilliseconds(),
submissions = emptyList(),
),
)
),
onCloseClicked = {},
onLoiReportAction = {},
) {
Box(modifier = Modifier.fillMaxSize().background(Color.LightGray)) {
Text(text = PAGER_CONTENT_TEXT, modifier = Modifier.align(Alignment.Center))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ import org.groundplatform.domain.model.task.Task
import org.groundplatform.domain.repository.SubmissionRepositoryInterface
import org.groundplatform.domain.usecases.GetLoiReportUseCase
import org.groundplatform.domain.usecases.submission.SubmitDataUseCase
import org.groundplatform.feature.pdf.LoiReportExporter
import org.groundplatform.ui.components.loireport.LoiReportAction
import timber.log.Timber

sealed interface DataCollectionUiEffect {
Expand All @@ -69,6 +71,8 @@ sealed interface DataCollectionUiEffect {
data class SetAwaitingPhotoCapture(val awaiting: Boolean) : DataCollectionUiEffect

data class ShowValidationError(val errorResId: Int) : DataCollectionUiEffect

data object ShowReportExportError : DataCollectionUiEffect
}

/** View model for the Data Collection fragment. */
Expand All @@ -86,6 +90,7 @@ internal constructor(
private val viewModelFactory: ViewModelFactory,
private val dataCollectionInitializer: DataCollectionInitializer,
private val getLoiReportUseCase: GetLoiReportUseCase,
private val loiReportExporter: LoiReportExporter,
) : AbstractViewModel() {

private val _uiEffects = Channel<DataCollectionUiEffect>(Channel.BUFFERED)
Expand Down Expand Up @@ -200,6 +205,15 @@ internal constructor(
}
}

fun onLoiReportAction(action: LoiReportAction) {
val loiReport = (uiState.value as? DataCollectionUiState.TaskSubmitted)?.loiReport
viewModelScope.launch {
if (loiReport == null || loiReportExporter.export(loiReport, action).isFailure) {
_uiEffects.send(DataCollectionUiEffect.ShowReportExportError)
}
}
}

fun handleLoiNameAction(action: LoiNameAction, taskId: String) {
when (action) {
is LoiNameAction.Confirmed -> {
Expand Down
Loading