diff --git a/app/src/main/java/org/groundplatform/android/ui/syncstatus/SyncStatusFragment.kt b/app/src/main/java/org/groundplatform/android/ui/syncstatus/SyncStatusFragment.kt index 97b40eb407..b306e8ff33 100644 --- a/app/src/main/java/org/groundplatform/android/ui/syncstatus/SyncStatusFragment.kt +++ b/app/src/main/java/org/groundplatform/android/ui/syncstatus/SyncStatusFragment.kt @@ -19,20 +19,11 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.semantics.testTag +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.fragment.findNavController import dagger.hilt.android.AndroidEntryPoint -import org.groundplatform.android.databinding.SyncStatusFragBinding import org.groundplatform.android.ui.common.AbstractFragment -import org.groundplatform.android.util.setComposableContent /** * This fragment summarizes the synchronization statuses of local changes that are being uploaded to @@ -54,21 +45,14 @@ class SyncStatusFragment : AbstractFragment() { savedInstanceState: Bundle?, ): View { super.onCreateView(inflater, container, savedInstanceState) - val binding = SyncStatusFragBinding.inflate(inflater, container, false) - binding.viewModel = viewModel - binding.lifecycleOwner = this - binding.composeView.setComposableContent { ShowSyncItems() } - getAbstractActivity().setSupportActionBar(binding.syncStatusToolbar) - return binding.root - } - - @Composable - private fun ShowSyncItems() { - val list by viewModel.uploadStatus.observeAsState() - list?.let { - LazyColumn(Modifier.fillMaxSize().testTag("sync list")) { - items(it) { - SyncListItem(modifier = Modifier.semantics { testTag = "item ${it.user}" }, detail = it) + return androidx.compose.ui.platform.ComposeView(requireContext()).apply { + setViewCompositionStrategy( + androidx.compose.ui.platform.ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed + ) + setContent { + org.groundplatform.android.ui.theme.AppTheme { + val list by viewModel.uploadStatus.collectAsStateWithLifecycle() + SyncStatusScreen(uploadStatuses = list, onBack = { findNavController().navigateUp() }) } } } diff --git a/app/src/main/java/org/groundplatform/android/ui/syncstatus/SyncStatusScreen.kt b/app/src/main/java/org/groundplatform/android/ui/syncstatus/SyncStatusScreen.kt new file mode 100644 index 0000000000..9deeab0f2e --- /dev/null +++ b/app/src/main/java/org/groundplatform/android/ui/syncstatus/SyncStatusScreen.kt @@ -0,0 +1,62 @@ +/* + * 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.ui.syncstatus + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTag +import org.groundplatform.android.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SyncStatusScreen(uploadStatuses: List, onBack: () -> Unit) { + Scaffold( + topBar = { + TopAppBar( + title = { Text(text = stringResource(R.string.data_sync_status)) }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.previous), + ) + } + }, + ) + } + ) { paddingValues -> + LazyColumn(modifier = Modifier.fillMaxSize().padding(paddingValues).testTag("sync list")) { + items(uploadStatuses) { + SyncListItem(modifier = Modifier.semantics { testTag = "item ${it.user}" }, detail = it) + } + } + } +} diff --git a/app/src/main/java/org/groundplatform/android/ui/syncstatus/SyncStatusViewModel.kt b/app/src/main/java/org/groundplatform/android/ui/syncstatus/SyncStatusViewModel.kt index 693bd5a1d2..e4ebf485ef 100644 --- a/app/src/main/java/org/groundplatform/android/ui/syncstatus/SyncStatusViewModel.kt +++ b/app/src/main/java/org/groundplatform/android/ui/syncstatus/SyncStatusViewModel.kt @@ -15,10 +15,12 @@ */ package org.groundplatform.android.ui.syncstatus -import androidx.lifecycle.LiveData -import androidx.lifecycle.asLiveData +import androidx.lifecycle.viewModelScope import javax.inject.Inject +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import org.groundplatform.android.model.mutation.LocationOfInterestMutation import org.groundplatform.android.model.mutation.SubmissionMutation import org.groundplatform.android.model.submission.UploadQueueEntry @@ -51,11 +53,11 @@ internal constructor( * A complete list of [SyncStatusDetail] indicating the current status of local changes being * synced to remote servers. */ - internal val uploadStatus: LiveData> = + internal val uploadStatus: StateFlow> = mutationRepository .getUploadQueueFlow() .map { it.mapNotNull { upload -> toSyncStatusDetail(upload) } } - .asLiveData() + .stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) private suspend fun toSyncStatusDetail(uploadQueueEntry: UploadQueueEntry): SyncStatusDetail? { val mutation = diff --git a/app/src/main/java/org/groundplatform/android/util/ComposeExt.kt b/app/src/main/java/org/groundplatform/android/util/ComposeExt.kt index a5549f4d44..98fce39aa6 100644 --- a/app/src/main/java/org/groundplatform/android/util/ComposeExt.kt +++ b/app/src/main/java/org/groundplatform/android/util/ComposeExt.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025 Google LLC + * 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. diff --git a/app/src/main/res/layout/sync_status_frag.xml b/app/src/main/res/layout/sync_status_frag.xml deleted file mode 100644 index a4d7f90efc..0000000000 --- a/app/src/main/res/layout/sync_status_frag.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/app/src/test/java/org/groundplatform/android/ui/syncstatus/SyncStatusFragmentTest.kt b/app/src/test/java/org/groundplatform/android/ui/syncstatus/SyncStatusFragmentTest.kt index a8345739ab..768b949926 100644 --- a/app/src/test/java/org/groundplatform/android/ui/syncstatus/SyncStatusFragmentTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/syncstatus/SyncStatusFragmentTest.kt @@ -15,13 +15,15 @@ */ package org.groundplatform.android.ui.syncstatus +import android.content.Context +import android.util.Log import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.ViewMatchers.isDisplayed -import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.work.Configuration +import androidx.work.testing.SynchronousExecutor +import androidx.work.testing.WorkManagerTestInitHelper +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.testing.HiltAndroidTest import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -51,6 +53,7 @@ import org.robolectric.RobolectricTestRunner class SyncStatusFragmentTest : BaseHiltTest() { @Inject lateinit var fakeRemoteDataStore: FakeRemoteDataStore + @Inject @ApplicationContext lateinit var context: Context @Inject lateinit var localLoiStore: LocalLocationOfInterestStore @Inject lateinit var localSubmissionStore: LocalSubmissionStore @Inject lateinit var localSurveyStore: LocalSurveyStore @@ -61,7 +64,12 @@ class SyncStatusFragmentTest : BaseHiltTest() { fun `Toolbar should be displayed`() { setupFragment() - onView(withId(R.id.sync_status_toolbar)).check(matches(isDisplayed())) + composeTestRule + .onNodeWithText( + androidx.test.core.app.ApplicationProvider.getApplicationContext() + .getString(R.string.data_sync_status) + ) + .assertIsDisplayed() } @Test @@ -114,6 +122,13 @@ class SyncStatusFragmentTest : BaseHiltTest() { } private fun setupFragment() = runWithTestDispatcher { + val config = + Configuration.Builder() + .setMinimumLoggingLevel(Log.INFO) + .setExecutor(SynchronousExecutor()) + .build() + WorkManagerTestInitHelper.initializeTestWorkManager(context, config) + launchFragmentInHiltContainer() advanceUntilIdle() } diff --git a/gradle.properties b/gradle.properties index 3342dc8e17..28f4b55b18 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,6 +10,7 @@ # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true + #Thu Dec 26 12:25:19 EST 2019 android.enableJetifier=true android.useAndroidX=true