From 78810569fa9bdb7d37593bca5c06449b06ea05b8 Mon Sep 17 00:00:00 2001 From: hue Date: Tue, 21 May 2024 15:20:07 -0400 Subject: [PATCH 01/11] Load large photos at reduced resolution --- .../com/crisiscleanup/sandbox/MultiImageViewModel.kt | 5 ++--- .../com/crisiscleanup/sandbox/SingleImageViewModel.kt | 4 +--- .../core/commoncase/ui/ExistingWorksitesList.kt | 1 + .../feature/mediamanage/ViewImageViewModel.kt | 9 ++++----- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/app-sandbox/src/main/java/com/crisiscleanup/sandbox/MultiImageViewModel.kt b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/MultiImageViewModel.kt index c550e0521..e4be7810e 100644 --- a/app-sandbox/src/main/java/com/crisiscleanup/sandbox/MultiImageViewModel.kt +++ b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/MultiImageViewModel.kt @@ -7,7 +7,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import coil.ImageLoader import coil.request.ImageRequest -import coil.size.Precision import com.crisiscleanup.core.designsystem.component.ViewImageViewState import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext @@ -81,8 +80,6 @@ class MultiImageViewModel @Inject constructor( callbackFlow { val request = ImageRequest.Builder(context) .data(url) - .size(Int.MAX_VALUE) - .precision(Precision.INEXACT) .target( onStart = { channel.trySend(ViewImageViewState.Loading) @@ -90,6 +87,8 @@ class MultiImageViewModel @Inject constructor( onSuccess = { result -> val bitmap = (result as BitmapDrawable).bitmap.asImageBitmap() ensureActive() + bitmap.prepareToDraw() + ensureActive() channel.trySend(ViewImageViewState.Image(url, bitmap)) }, onError = { diff --git a/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SingleImageViewModel.kt b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SingleImageViewModel.kt index d6c20e838..6f4d947a5 100644 --- a/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SingleImageViewModel.kt +++ b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SingleImageViewModel.kt @@ -7,7 +7,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import coil.ImageLoader import coil.request.ImageRequest -import coil.size.Precision import com.crisiscleanup.core.designsystem.component.ViewImageViewState import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext @@ -37,14 +36,13 @@ class SingleImageViewModel @Inject constructor( callbackFlow { val request = ImageRequest.Builder(context) .data(url) - .size(Int.MAX_VALUE) - .precision(Precision.INEXACT) .target( onStart = { channel.trySend(ViewImageViewState.Loading) }, onSuccess = { result -> val bitmap = (result as BitmapDrawable).bitmap.asImageBitmap() + bitmap.prepareToDraw() channel.trySend(ViewImageViewState.Image(url, bitmap)) }, onError = { diff --git a/core/commoncase/src/main/java/com/crisiscleanup/core/commoncase/ui/ExistingWorksitesList.kt b/core/commoncase/src/main/java/com/crisiscleanup/core/commoncase/ui/ExistingWorksitesList.kt index a74a26e62..046d05009 100644 --- a/core/commoncase/src/main/java/com/crisiscleanup/core/commoncase/ui/ExistingWorksitesList.kt +++ b/core/commoncase/src/main/java/com/crisiscleanup/core/commoncase/ui/ExistingWorksitesList.kt @@ -32,6 +32,7 @@ private fun CaseView( with(caseSummary) { icon?.let { Image( + // TODO Cache image bitmap, prepareToDraw() as well bitmap = it.asImageBitmap(), contentDescription = summary.workType?.workTypeLiteral, ) diff --git a/feature/mediamanage/src/main/java/com/crisiscleanup/feature/mediamanage/ViewImageViewModel.kt b/feature/mediamanage/src/main/java/com/crisiscleanup/feature/mediamanage/ViewImageViewModel.kt index 865d242b1..5d831aa24 100644 --- a/feature/mediamanage/src/main/java/com/crisiscleanup/feature/mediamanage/ViewImageViewModel.kt +++ b/feature/mediamanage/src/main/java/com/crisiscleanup/feature/mediamanage/ViewImageViewModel.kt @@ -11,7 +11,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import coil.ImageLoader import coil.request.ImageRequest -import coil.size.Precision import com.crisiscleanup.core.appnav.ViewImageArgs import com.crisiscleanup.core.common.KeyResourceTranslator import com.crisiscleanup.core.common.NetworkMonitor @@ -220,10 +219,11 @@ fun ContentResolver.tryDecodeContentImage( openFileDescriptor(uri, "r").use { it?.let { parcel -> val fileDescriptor = parcel.fileDescriptor - val bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor) + val bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor).asImageBitmap() + bitmap.prepareToDraw() return ViewImageViewState.Image( uriString!!, - bitmap.asImageBitmap(), + bitmap, ) } } @@ -244,14 +244,13 @@ internal fun ImageLoader.queueNetworkImage( ) = callbackFlow { val request = ImageRequest.Builder(context) .data(imageUrl) - .size(Int.MAX_VALUE) - .precision(Precision.INEXACT) .target( onStart = { channel.trySend(ViewImageViewState.Loading) }, onSuccess = { result -> val bitmap = (result as BitmapDrawable).bitmap.asImageBitmap() + bitmap.prepareToDraw() channel.trySend(ViewImageViewState.Image(imageUrl, bitmap)) }, onError = { From 542ac97c074408060a1e0140f27989bbeda3b6f1 Mon Sep 17 00:00:00 2001 From: hue Date: Tue, 21 May 2024 15:24:28 -0400 Subject: [PATCH 02/11] Jump to next input in Case address input fields --- .../com/crisiscleanup/feature/caseeditor/ui/LocationScreen.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/LocationScreen.kt b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/LocationScreen.kt index 4004c0f7a..01d271864 100644 --- a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/LocationScreen.kt +++ b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/LocationScreen.kt @@ -358,7 +358,6 @@ private fun LocationAddressFormView( keyboardCapitalization = KeyboardCapitalization.Words, isError = isStateError, hasFocus = focusState, - imeAction = ImeAction.Done, onEnter = onStateEnd, enabled = isEditable, ) From 51e3a1c3545006097e44cb1a27eefd166b90c384 Mon Sep 17 00:00:00 2001 From: Aaron Titus Date: Wed, 22 May 2024 17:59:05 -0600 Subject: [PATCH 03/11] Android localizations --- .../feature/authentication/ui/LoginWithPhoneScreen.kt | 4 ++-- .../main/java/com/crisiscleanup/feature/menu/MenuScreen.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/LoginWithPhoneScreen.kt b/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/LoginWithPhoneScreen.kt index b2778cd5b..a8d431a9f 100644 --- a/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/LoginWithPhoneScreen.kt +++ b/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/LoginWithPhoneScreen.kt @@ -174,7 +174,7 @@ private fun LoginWithPhoneScreen( // TODO Hide if device does not have a SIM/phone number LinkAction( - "~~Use phone's number", + t("loginWithPhone.use_phones_number"), modifier = Modifier .listItemPadding() .testTag("phoneLoginRequestPhoneNumber"), @@ -271,7 +271,7 @@ private fun ColumnScope.VerifyPhoneCodeScreen( val updatePhoneCode = remember(viewModel) { { s: String -> viewModel.phoneCode = s.trim() } } OutlinedClearableTextField( modifier = listItemModifier.testTag("loginPhoneCodeTextField"), - label = t("~~Phone login code"), + label = t("loginWithPhone.phone_login_code"), value = viewModel.phoneCode.trim(), onValueChange = updatePhoneCode, keyboardType = KeyboardType.Number, diff --git a/feature/menu/src/main/java/com/crisiscleanup/feature/menu/MenuScreen.kt b/feature/menu/src/main/java/com/crisiscleanup/feature/menu/MenuScreen.kt index c7e74ef4b..ad278a8c0 100644 --- a/feature/menu/src/main/java/com/crisiscleanup/feature/menu/MenuScreen.kt +++ b/feature/menu/src/main/java/com/crisiscleanup/feature/menu/MenuScreen.kt @@ -342,7 +342,7 @@ private fun GettingStartedSection( .size(64.dp) .padding(8.dp), imageVector = CrisisCleanupIcons.Play, - contentDescription = t("~~Play getting started video"), + contentDescription = t("dashboard.play_getting_started_video"), ) } } From 3b32f45fac2eee85404d86d99a8115083ea7a522 Mon Sep 17 00:00:00 2001 From: hue Date: Thu, 23 May 2024 15:00:08 -0400 Subject: [PATCH 04/11] Update translation syntax --- app/build.gradle.kts | 2 +- .../authentication/ui/LoginWithPhoneScreen.kt | 8 ++++---- gradle.properties | 19 ++++++++++--------- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6a37f244c..d864f199b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -13,7 +13,7 @@ plugins { android { defaultConfig { - val buildVersion = 201 + val buildVersion = 202 applicationId = "com.crisiscleanup" versionCode = buildVersion versionName = "0.9.${buildVersion - 168}" diff --git a/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/LoginWithPhoneScreen.kt b/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/LoginWithPhoneScreen.kt index a8d431a9f..a78eeadd9 100644 --- a/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/LoginWithPhoneScreen.kt +++ b/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/LoginWithPhoneScreen.kt @@ -137,11 +137,11 @@ private fun LoginWithPhoneScreen( onBack: () -> Unit = {}, viewModel: LoginWithPhoneViewModel = hiltViewModel(), ) { - val translator = LocalAppTranslator.current + val t = LocalAppTranslator.current Text( modifier = listItemModifier.testTag("phoneLoginHeaderText"), - text = translator("actions.login", R.string.login), + text = t("actions.login", R.string.login), style = LocalFontStyles.current.header1, ) @@ -161,7 +161,7 @@ private fun LoginWithPhoneScreen( } OutlinedClearableTextField( modifier = fillWidthPadded.testTag("loginPhoneTextField"), - label = translator("loginWithPhone.enter_cell"), + label = t("loginWithPhone.enter_cell"), value = phoneNumber, onValueChange = updateEmailInput, keyboardType = KeyboardType.Phone, @@ -188,7 +188,7 @@ private fun LoginWithPhoneScreen( modifier = fillWidthPadded.testTag("phoneLoginAction"), onClick = requestPhoneCode, enabled = isNotBusy, - text = translator("loginForm.login_with_cell"), + text = t("loginForm.login_with_cell"), indicateBusy = isRequestingCode, ) diff --git a/gradle.properties b/gradle.properties index 622ef5b6b..c4e5d7fd1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,7 +7,7 @@ # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. # Ensure important default jvmargs aren't overwritten. See https://github.com/gradle/gradle/issues/19750 -org.gradle.jvmargs=-Xmx4096g -XX:MaxPermSize=4096m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:MaxMetaspaceSize=1g +org.gradle.jvmargs=-Xmx6g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:MaxMetaspaceSize=1g # When configured, Gradle will run in incubating parallel mode. # 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 @@ -17,21 +17,22 @@ org.gradle.configureondemand=false # Enable caching between builds. org.gradle.caching=true # Enable configuration caching between builds. -org.gradle.unsafe.configuration-cache=true +org.gradle.configuration-cache=true +# This option is set because of https://github.com/google/play-services-plugins/issues/246 +# to generate the Configuration Cache regardless of incompatible tasks. +# See https://github.com/android/nowinandroid/issues/1022 before using it. +org.gradle.configuration-cache.problems=warn # AndroidX package structure to make it clearer which packages are bundled with the # Android operating system, and which are packaged with your app"s APK # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official -# Non-transitive R classes is recommended and is faster/smaller -android.nonTransitiveRClass=true # Disable build features that are enabled by default, -# https://developer.android.com/studio/releases/gradle-plugin#buildFeatures -android.defaults.buildfeatures.buildconfig=false -android.defaults.buildfeatures.aidl=false -android.defaults.buildfeatures.renderscript=false +# https://developer.android.com/build/releases/gradle-plugin#default-changes android.defaults.buildfeatures.resvalues=false android.defaults.buildfeatures.shaders=false # TODO Enable once proguard rules related to Retrofit are provided -android.enableR8.fullMode=false +#android.enableR8.fullMode=false +# Run Roborazzi screenshot tests with the local tests +roborazzi.test.verify=true From fbac68ae2a9da75a2de7faee1fabddddb70880a3 Mon Sep 17 00:00:00 2001 From: hue Date: Thu, 23 May 2024 15:01:21 -0400 Subject: [PATCH 05/11] Log out when terms are not accepted and tokens are not valid Requires acceptance of terms or queries already accepted terms state. --- app/src/main/java/com/crisiscleanup/MainActivityViewModel.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/com/crisiscleanup/MainActivityViewModel.kt b/app/src/main/java/com/crisiscleanup/MainActivityViewModel.kt index cb19f49fe..f5a97586e 100644 --- a/app/src/main/java/com/crisiscleanup/MainActivityViewModel.kt +++ b/app/src/main/java/com/crisiscleanup/MainActivityViewModel.kt @@ -195,6 +195,10 @@ class MainActivityViewModel @Inject constructor( syncPuller.appPullIncident(incidentSelector.incidentId.first()) accountDataRefresher.updateMyOrganization(true) accountDataRefresher.updateApprovedIncidents() + + if (!it.hasAcceptedTerms && !it.areTokensValid) { + authEventBus.onLogout() + } } .launchIn(viewModelScope) From 2f171c718bad21e3b34d32f3217a4d1f43cf05d2 Mon Sep 17 00:00:00 2001 From: hue Date: Thu, 23 May 2024 17:14:04 -0400 Subject: [PATCH 06/11] Select system language as default in org invite form --- .../LanguageTranslationsRepository.kt | 22 +++++++++++++++++++ .../OrgPersistentInviteViewModel.kt | 2 +- .../RequestOrgAccessViewModel.kt | 2 +- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/core/data/src/main/java/com/crisiscleanup/core/data/repository/LanguageTranslationsRepository.kt b/core/data/src/main/java/com/crisiscleanup/core/data/repository/LanguageTranslationsRepository.kt index 342b8e0dc..ccd537228 100644 --- a/core/data/src/main/java/com/crisiscleanup/core/data/repository/LanguageTranslationsRepository.kt +++ b/core/data/src/main/java/com/crisiscleanup/core/data/repository/LanguageTranslationsRepository.kt @@ -52,6 +52,8 @@ interface LanguageTranslationsRepository : KeyTranslator { fun setLanguageFromSystem() suspend fun getLanguageOptions(): List + + fun getRecommendedLanguage(languageOptions: List): LanguageIdName } @Singleton @@ -216,6 +218,26 @@ class OfflineFirstLanguageTranslationsRepository @Inject constructor( return emptyList() } + override fun getRecommendedLanguage(languageOptions: List): LanguageIdName { + val systemLocale = Locale.getDefault().toLanguageTag() + val languageLookup = languageOptions + .associateBy { + it.name.split(".").last() + } + val localeLower = systemLocale.lowercase() + languageLookup[localeLower]?.let { + return it + } + + languageOptions.firstOrNull { + it.name.contains("en-us") + }?.let { + return it + } + + return languageOptions.first() + } + override fun translate(phraseKey: String): String? { translations.value[phraseKey]?.let { return it diff --git a/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/OrgPersistentInviteViewModel.kt b/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/OrgPersistentInviteViewModel.kt index e21337015..67e11f5ef 100644 --- a/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/OrgPersistentInviteViewModel.kt +++ b/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/OrgPersistentInviteViewModel.kt @@ -99,7 +99,7 @@ class OrgPersistentInviteViewModel @Inject constructor( languageOptions .onEach { if (it.isNotEmpty() && userInfo.language.name.isBlank()) { - userInfo.language = it.first() + userInfo.language = languageRepository.getRecommendedLanguage(it) } } .launchIn(viewModelScope) diff --git a/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/RequestOrgAccessViewModel.kt b/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/RequestOrgAccessViewModel.kt index 1c9434497..cb4520648 100644 --- a/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/RequestOrgAccessViewModel.kt +++ b/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/RequestOrgAccessViewModel.kt @@ -135,7 +135,7 @@ class RequestOrgAccessViewModel @Inject constructor( languageOptions .onEach { if (it.isNotEmpty() && userInfo.language.name.isBlank()) { - userInfo.language = it.first() + userInfo.language = languageRepository.getRecommendedLanguage(it) } } .launchIn(viewModelScope) From 1dab737a9b9f6193b94e6a30542786c340b0336c Mon Sep 17 00:00:00 2001 From: hue Date: Thu, 23 May 2024 18:31:28 -0400 Subject: [PATCH 07/11] Downsize large local photos without crashing --- .../feature/mediamanage/ViewImageViewModel.kt | 32 +++++++++++++++++-- gradle.properties | 2 -- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/feature/mediamanage/src/main/java/com/crisiscleanup/feature/mediamanage/ViewImageViewModel.kt b/feature/mediamanage/src/main/java/com/crisiscleanup/feature/mediamanage/ViewImageViewModel.kt index 5d831aa24..7fd87efe5 100644 --- a/feature/mediamanage/src/main/java/com/crisiscleanup/feature/mediamanage/ViewImageViewModel.kt +++ b/feature/mediamanage/src/main/java/com/crisiscleanup/feature/mediamanage/ViewImageViewModel.kt @@ -6,6 +6,7 @@ import android.graphics.BitmapFactory import android.graphics.drawable.BitmapDrawable import android.net.Uri import androidx.compose.ui.graphics.asImageBitmap +import androidx.core.graphics.scale import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -50,6 +51,11 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.concurrent.atomic.AtomicReference import javax.inject.Inject +import kotlin.math.ceil +import kotlin.math.ln +import kotlin.math.max +import kotlin.math.pow +import kotlin.math.roundToInt @OptIn(FlowPreview::class) @HiltViewModel @@ -219,11 +225,31 @@ fun ContentResolver.tryDecodeContentImage( openFileDescriptor(uri, "r").use { it?.let { parcel -> val fileDescriptor = parcel.fileDescriptor - val bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor).asImageBitmap() - bitmap.prepareToDraw() + var bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor) + + val maxDimension = max(bitmap.width, bitmap.height) + // TODO Review max length + val maxDimensionLength = 3840.0 + if (maxDimension > maxDimensionLength) { + val scale = maxDimension / maxDimensionLength + val scalePow2 = ceil(ln(scale) / ln(2.0)) + if (scalePow2 > 0) { + val actualScale = 1.0 / 2.0.pow(scalePow2) + val scaleWidth = (actualScale * bitmap.width).roundToInt() + val scaleHeight = (actualScale * bitmap.height).roundToInt() + bitmap = bitmap.scale( + scaleWidth, + scaleHeight, + false, + ) + } + } + + val imageBitmap = bitmap.asImageBitmap() + imageBitmap.prepareToDraw() return ViewImageViewState.Image( uriString!!, - bitmap, + imageBitmap, ) } } diff --git a/gradle.properties b/gradle.properties index c4e5d7fd1..96414c576 100644 --- a/gradle.properties +++ b/gradle.properties @@ -32,7 +32,5 @@ kotlin.code.style=official # https://developer.android.com/build/releases/gradle-plugin#default-changes android.defaults.buildfeatures.resvalues=false android.defaults.buildfeatures.shaders=false -# TODO Enable once proguard rules related to Retrofit are provided -#android.enableR8.fullMode=false # Run Roborazzi screenshot tests with the local tests roborazzi.test.verify=true From 89497c2c47197ddd533f26b3c4728d810230b375 Mon Sep 17 00:00:00 2001 From: hue Date: Thu, 23 May 2024 18:36:34 -0400 Subject: [PATCH 08/11] Open view Case Info to first list item --- .../crisiscleanup/feature/caseeditor/ui/ViewCaseScreen.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ViewCaseScreen.kt b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ViewCaseScreen.kt index d2f855df4..14ec2bf8f 100644 --- a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ViewCaseScreen.kt +++ b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ViewCaseScreen.kt @@ -584,7 +584,8 @@ private fun CaseInfoView( val distanceAwayText by viewModel.distanceAwayText.collectAsStateWithLifecycle() - LazyColumn { + val listState = rememberLazyListState() + LazyColumn(state = listState) { item(key = "incident-info") { val caseData by viewModel.caseData.collectAsStateWithLifecycle() caseData?.let { caseState -> @@ -597,6 +598,10 @@ private fun CaseInfoView( isSyncing = isSyncing, scheduleSync = scheduleSync, ) + + LaunchedEffect(Unit) { + listState.scrollToItem(0) + } } } From 7a54282932e11d48b0bf5e87831dd45e124288ba Mon Sep 17 00:00:00 2001 From: hue Date: Fri, 24 May 2024 12:48:02 -0400 Subject: [PATCH 09/11] Assume address changes only if at least partially defined --- app/build.gradle.kts | 2 +- .../caseeditor/model/LocationInputData.kt | 31 +++++++++++++------ 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d864f199b..4fe220db1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -13,7 +13,7 @@ plugins { android { defaultConfig { - val buildVersion = 202 + val buildVersion = 203 applicationId = "com.crisiscleanup" versionCode = buildVersion versionName = "0.9.${buildVersion - 168}" diff --git a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/model/LocationInputData.kt b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/model/LocationInputData.kt index fe386b76e..67db8841e 100644 --- a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/model/LocationInputData.kt +++ b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/model/LocationInputData.kt @@ -211,18 +211,29 @@ class LocationInputData( referenceWorksite = referenceWorksite.copy( latitude = worksite.latitude, longitude = worksite.longitude, - address = worksite.address, - city = worksite.city, - county = worksite.county, - postalCode = worksite.postalCode, - state = worksite.state, ) coordinates.value = LatLng(worksite.latitude, worksite.longitude) - streetAddress = worksite.address - zipCode = worksite.postalCode - city = worksite.city - county = worksite.county - state = worksite.state + + if ( + worksite.address.isNotBlank() || + worksite.city.isNotBlank() || + worksite.county.isNotBlank() || + worksite.postalCode.isNotBlank() || + worksite.state.isNotBlank() + ) { + referenceWorksite = referenceWorksite.copy( + address = worksite.address, + city = worksite.city, + county = worksite.county, + postalCode = worksite.postalCode, + state = worksite.state, + ) + streetAddress = worksite.address + zipCode = worksite.postalCode + city = worksite.city + county = worksite.county + state = worksite.state + } if (isIncompleteAddress) { isEditingAddress = true From b492625992cb7da5d175872c056c9ae0c95f25fe Mon Sep 17 00:00:00 2001 From: hue Date: Fri, 24 May 2024 20:05:16 -0400 Subject: [PATCH 10/11] Focus on search input when no recents exist --- .../feature/cases/CasesSearchViewModel.kt | 18 ++++++++++++++++++ .../feature/cases/ui/CasesSearchScreen.kt | 9 ++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/CasesSearchViewModel.kt b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/CasesSearchViewModel.kt index 4f399e7ac..c2fab8cc2 100644 --- a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/CasesSearchViewModel.kt +++ b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/CasesSearchViewModel.kt @@ -1,6 +1,9 @@ package com.crisiscleanup.feature.cases import android.graphics.Bitmap +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.crisiscleanup.core.common.combine @@ -31,14 +34,18 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.flow.combine as kCombine +@OptIn(FlowPreview::class) @HiltViewModel class CasesSearchViewModel @Inject constructor( private val incidentSelector: IncidentSelector, @@ -297,10 +304,21 @@ class CasesSearchViewModel @Inject constructor( started = SharingStarted.WhileSubscribed(), ) + var focusOnSearchInput by mutableStateOf(false) + private set + init { viewModelScope.launch(ioDispatcher) { databaseManagementRepository.rebuildFts() } + + recentWorksites + .debounce(0.6.seconds) + .filter { it.isEmpty() } + .onEach { + focusOnSearchInput = true + } + .launchIn(viewModelScope) } private fun getIcon(workType: WorkType?) = workType?.let { diff --git a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesSearchScreen.kt b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesSearchScreen.kt index 1112d1b8a..df4f0739b 100644 --- a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesSearchScreen.kt +++ b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesSearchScreen.kt @@ -78,8 +78,9 @@ internal fun CasesSearchRoute( val recentCases by viewModel.recentWorksites.collectAsStateWithLifecycle() val searchResults by viewModel.searchResults.collectAsStateWithLifecycle() - val closeKeyboard = rememberCloseKeyboard(viewModel) + val focusOnSearchInput = viewModel.focusOnSearchInput + val closeKeyboard = rememberCloseKeyboard(viewModel) val isEditable = !isSelectingResult if (isListDetailLayout) { Row { @@ -88,6 +89,7 @@ internal fun CasesSearchRoute( q, updateQuery, isEditable, + hasFocus = focusOnSearchInput, closeKeyboard, onCaseSelect, emptyList(), @@ -121,6 +123,7 @@ internal fun CasesSearchRoute( q, updateQuery, isEditable, + hasFocus = focusOnSearchInput, closeKeyboard, onCaseSelect, recentCases, @@ -139,6 +142,7 @@ private fun SearchCasesView( q: String, updateQuery: (String) -> Unit, isEditable: Boolean, + hasFocus: Boolean, closeKeyboard: () -> Unit, onCaseSelect: (CaseSummaryResult) -> Unit, recentCases: List, @@ -155,6 +159,7 @@ private fun SearchCasesView( q, updateQuery, isEditable, + hasFocus = hasFocus, closeKeyboard, ) @@ -179,6 +184,7 @@ private fun SearchBar( q: String, updateQuery: (String) -> Unit, isEditable: Boolean, + hasFocus: Boolean, closeKeyboard: () -> Unit, ) { val t = LocalAppTranslator.current @@ -210,6 +216,7 @@ private fun SearchBar( enabled = isEditable, imeAction = ImeAction.Done, onEnter = closeKeyboard, + hasFocus = hasFocus, isError = false, ) } From ffe5130c478ef398c04de450b55f64b454110461 Mon Sep 17 00:00:00 2001 From: hue Date: Fri, 24 May 2024 20:28:20 -0400 Subject: [PATCH 11/11] Guard against back navigation twice from search Cases view --- .../feature/cases/ui/CasesSearchScreen.kt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesSearchScreen.kt b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesSearchScreen.kt index df4f0739b..b48c7bb37 100644 --- a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesSearchScreen.kt +++ b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesSearchScreen.kt @@ -13,7 +13,9 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag @@ -48,6 +50,14 @@ internal fun CasesSearchRoute( openCase: (Long, Long) -> Boolean = { _, _ -> false }, viewModel: CasesSearchViewModel = hiltViewModel(), ) { + // Guard due to navigation animations + var onBackCount by remember { mutableIntStateOf(0) } + val onSingleBack = { + if (onBackCount++ == 0) { + onBackClick() + } + } + val selectedWorksite by viewModel.selectedWorksite.collectAsStateWithLifecycle() if (selectedWorksite.second != EmptyWorksite.id) { openCase(selectedWorksite.first, selectedWorksite.second) @@ -85,7 +95,7 @@ internal fun CasesSearchRoute( if (isListDetailLayout) { Row { SearchCasesView( - onBackClick, + onSingleBack, q, updateQuery, isEditable, @@ -119,7 +129,7 @@ internal fun CasesSearchRoute( } } else { SearchCasesView( - onBackClick, + onSingleBack, q, updateQuery, isEditable,