From 0fb1fbabb0e57165f3e6cf2bbf0aa54db8a8e70b Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Fri, 26 Apr 2024 17:08:01 -0700 Subject: [PATCH 01/32] make formelements observable --- gradle.properties | 2 +- .../toolkit/featureforms/FeatureForm.kt | 58 +++++++++---------- .../AttachmentElementDefaults.kt | 22 +++---- .../attachment/AttachmentElementState.kt | 30 ++++++++++ .../AttachmentFormElement.kt | 24 ++++---- .../components/text/FormTextFieldState.kt | 2 + 6 files changed, 82 insertions(+), 56 deletions(-) rename toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/{formelement => attachment}/AttachmentElementDefaults.kt (64%) create mode 100644 toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt rename toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/{formelement => attachment}/AttachmentFormElement.kt (96%) diff --git a/gradle.properties b/gradle.properties index a3f9237b2..49d3649bd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -54,4 +54,4 @@ ignoreBuildNumber=false # these versions define the dependency of the ArcGIS Maps SDK for Kotlin dependency # and are generally not overridden at the command line unless a special build is requested. sdkVersionNumber=200.5.0 -sdkBuildNumber=4212 +sdkBuildNumber=4220 diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/FeatureForm.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/FeatureForm.kt index dca43c6c5..6a7048ec2 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/FeatureForm.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/FeatureForm.kt @@ -18,6 +18,7 @@ package com.arcgismaps.toolkit.featureforms +import android.util.Log import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -38,11 +39,13 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer @@ -55,11 +58,15 @@ import com.arcgismaps.mapping.featureforms.ComboBoxFormInput import com.arcgismaps.mapping.featureforms.DateTimePickerFormInput import com.arcgismaps.mapping.featureforms.FeatureForm import com.arcgismaps.mapping.featureforms.FieldFormElement +import com.arcgismaps.mapping.featureforms.FormElement import com.arcgismaps.mapping.featureforms.GroupFormElement import com.arcgismaps.mapping.featureforms.RadioButtonsFormInput import com.arcgismaps.mapping.featureforms.SwitchFormInput import com.arcgismaps.mapping.featureforms.TextAreaFormInput import com.arcgismaps.mapping.featureforms.TextBoxFormInput +import com.arcgismaps.toolkit.featureforms.internal.components.attachment.AttachmentFormElement +import com.arcgismaps.toolkit.featureforms.internal.components.attachment.FakeAttachmentElementState +import com.arcgismaps.toolkit.featureforms.internal.components.attachment.fakeAttachments import com.arcgismaps.toolkit.featureforms.internal.components.base.BaseFieldState import com.arcgismaps.toolkit.featureforms.internal.components.base.BaseGroupState import com.arcgismaps.toolkit.featureforms.internal.components.base.FormStateCollection @@ -70,14 +77,12 @@ import com.arcgismaps.toolkit.featureforms.internal.components.codedvalue.rememb import com.arcgismaps.toolkit.featureforms.internal.components.codedvalue.rememberRadioButtonFieldState import com.arcgismaps.toolkit.featureforms.internal.components.codedvalue.rememberSwitchFieldState import com.arcgismaps.toolkit.featureforms.internal.components.datetime.rememberDateTimeFieldState -import com.arcgismaps.toolkit.featureforms.internal.components.formelement.AttachmentFormElement -import com.arcgismaps.toolkit.featureforms.internal.components.formelement.FakeAttachmentElementState import com.arcgismaps.toolkit.featureforms.internal.components.formelement.FieldElement import com.arcgismaps.toolkit.featureforms.internal.components.formelement.GroupElement -import com.arcgismaps.toolkit.featureforms.internal.components.formelement.fakeAttachments import com.arcgismaps.toolkit.featureforms.internal.components.text.rememberFormTextFieldState import com.arcgismaps.toolkit.featureforms.internal.utils.FeatureFormDialog import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay /** * The "property" determines the behavior of when the validation errors are visible. @@ -129,6 +134,7 @@ public fun FeatureForm( validationErrorVisibility = validationErrorVisibility ) } + /** * A wrapper to hold state data. This provides a [Stable] class to enable smart recompositions, * since [FeatureForm] is not stable. @@ -146,18 +152,24 @@ internal fun FeatureForm( validationErrorVisibility: ValidationErrorVisibility = ValidationErrorVisibility.Automatic ) { val featureForm = stateData.featureForm + // hold the list of form elements in a mutable state to make them observable + val formElements = remember(featureForm) { + mutableStateOf(featureForm.elements) + } val scope = rememberCoroutineScope() - - val states = rememberStates( form = featureForm, + elements = formElements.value, scope = scope ) FeatureFormBody( form = featureForm, states = states, modifier = modifier - ) + ) { + // expressions evaluated, load attachments + formElements.value = featureForm.elements + } FeatureFormDialog() // launch a new side effect in a launched effect when validationErrorVisibility changes LaunchedEffect(validationErrorVisibility) { @@ -193,14 +205,11 @@ private fun FeatureFormTitle(featureForm: FeatureForm, modifier: Modifier = Modi private fun FeatureFormBody( form: FeatureForm, states: FormStateCollection, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + onExpressionsEvaluated: () -> Unit ) { var initialEvaluation by rememberSaveable(form) { mutableStateOf(false) } val lazyListState = rememberLazyListState() - - if (initialEvaluation) { - rememberAttachmentStates(form = form, states = states) - } Column( modifier = modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally @@ -241,7 +250,7 @@ private fun FeatureFormBody( .padding(horizontal = 15.dp, vertical = 10.dp) ) } - + is AttachmentFormElement -> { AttachmentFormElement( Modifier @@ -262,6 +271,7 @@ private fun FeatureFormBody( LaunchedEffect(form) { // ensure expressions are evaluated form.evaluateExpressions() + onExpressionsEvaluated() initialEvaluation = true } } @@ -296,10 +306,11 @@ internal fun InitializingExpressions( @Composable internal fun rememberStates( form: FeatureForm, + elements : List, scope: CoroutineScope ): FormStateCollection { val states = MutableFormStateCollection() - form.elements.forEach { element -> + elements.forEach { element -> when (element) { is FieldFormElement -> { val state = rememberFieldState(element, form, scope) @@ -331,32 +342,17 @@ internal fun rememberStates( } is AttachmentFormElement -> { - val state = rememberFakeAttachmentElementState(form = form, attachmentFormElement = element) + val state = + rememberFakeAttachmentElementState(form = form, attachmentFormElement = element) states.add(element, state) } - else -> { } - } - } - return states -} - -@Composable -internal fun rememberAttachmentStates( - form: FeatureForm, - states: FormStateCollection -): FormStateCollection { - form.elements.filterIsInstance().forEach { element -> - if (states[element] == null) { - val state = - rememberFakeAttachmentElementState(form = form, attachmentFormElement = element) - (states as MutableFormStateCollection).add(element, state) + else -> {} } } return states } - /** * Creates and remembers a [BaseFieldState] for the provided [element]. * diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/formelement/AttachmentElementDefaults.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementDefaults.kt similarity index 64% rename from toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/formelement/AttachmentElementDefaults.kt rename to toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementDefaults.kt index bdc0ec752..3e80305db 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/formelement/AttachmentElementDefaults.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementDefaults.kt @@ -1,20 +1,20 @@ /* - * COPYRIGHT 1995-2024 ESRI + * Copyright 2024 Esri * - * TRADE SECRETS: ESRI PROPRIETARY AND CONFIDENTIAL - * Unpublished material - all rights reserved under the - * Copyright Laws of the United States. + * 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 * - * For additional information, contact: - * Environmental Systems Research Institute, Inc. - * Attn: Contracts Dept - * 380 New York Street - * Redlands, California, USA 92373 + * http://www.apache.org/licenses/LICENSE-2.0 * - * email: contracts@esri.com + * 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 com.arcgismaps.toolkit.featureforms.internal.components.formelement +package com.arcgismaps.toolkit.featureforms.internal.components.attachment import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt new file mode 100644 index 000000000..1a6f0c89e --- /dev/null +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2024 Esri + * + * 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 + * + * http://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 com.arcgismaps.toolkit.featureforms.internal.components.attachment + +import com.arcgismaps.mapping.featureforms.AttachmentFormElement +import com.arcgismaps.toolkit.featureforms.internal.components.base.FormElementState + +internal class AttachmentElementState( + private val formElement : AttachmentFormElement +) : FormElementState( + label = formElement.label, + description = formElement.description, + isVisible = formElement.isVisible, +){ + val keyword = formElement.keyword +} diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/formelement/AttachmentFormElement.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt similarity index 96% rename from toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/formelement/AttachmentFormElement.kt rename to toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt index 50e04209e..201df7584 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/formelement/AttachmentFormElement.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt @@ -1,22 +1,20 @@ /* + * Copyright 2024 Esri * - * Copyright 2024 Esri + * 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 * - * 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 - * - * http://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. + * http://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 com.arcgismaps.toolkit.featureforms.internal.components.formelement +package com.arcgismaps.toolkit.featureforms.internal.components.attachment import androidx.compose.animation.core.Spring import androidx.compose.animation.core.animateFloat diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/text/FormTextFieldState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/text/FormTextFieldState.kt index d0b739132..dd8ff4a64 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/text/FormTextFieldState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/text/FormTextFieldState.kt @@ -16,6 +16,7 @@ package com.arcgismaps.toolkit.featureforms.internal.components.text +import android.util.Log import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.saveable.Saver @@ -172,6 +173,7 @@ internal fun rememberFormTextFieldState( inputs = arrayOf(form), saver = FormTextFieldState.Saver(field, form, scope) ) { + Log.e("TAG", "rememberFormTextFieldState: init for ${field.label}", ) FormTextFieldState( properties = TextFieldProperties( label = field.label, From de2abfce707ab422144984a82189f4c295397dc0 Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Mon, 29 Apr 2024 16:11:56 -0700 Subject: [PATCH 02/32] add attachment loading --- .../toolkit/featureforms/FeatureForm.kt | 22 +- .../attachment/AttachmentElementState.kt | 145 +++++- .../attachment/AttachmentFormElement.kt | 420 ++---------------- .../components/attachment/AttachmentTile.kt | 308 +++++++++++++ 4 files changed, 501 insertions(+), 394 deletions(-) create mode 100644 toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/FeatureForm.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/FeatureForm.kt index 6a7048ec2..c3c74b226 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/FeatureForm.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/FeatureForm.kt @@ -41,6 +41,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.neverEqualPolicy import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable @@ -64,9 +65,9 @@ import com.arcgismaps.mapping.featureforms.RadioButtonsFormInput import com.arcgismaps.mapping.featureforms.SwitchFormInput import com.arcgismaps.mapping.featureforms.TextAreaFormInput import com.arcgismaps.mapping.featureforms.TextBoxFormInput +import com.arcgismaps.toolkit.featureforms.internal.components.attachment.AttachmentElementState import com.arcgismaps.toolkit.featureforms.internal.components.attachment.AttachmentFormElement -import com.arcgismaps.toolkit.featureforms.internal.components.attachment.FakeAttachmentElementState -import com.arcgismaps.toolkit.featureforms.internal.components.attachment.fakeAttachments +import com.arcgismaps.toolkit.featureforms.internal.components.attachment.rememberAttachmentElementState import com.arcgismaps.toolkit.featureforms.internal.components.base.BaseFieldState import com.arcgismaps.toolkit.featureforms.internal.components.base.BaseGroupState import com.arcgismaps.toolkit.featureforms.internal.components.base.FormStateCollection @@ -253,6 +254,7 @@ private fun FeatureFormBody( is AttachmentFormElement -> { AttachmentFormElement( + state = entry.getState(), Modifier .fillMaxWidth() .padding(horizontal = 15.dp, vertical = 10.dp) @@ -271,8 +273,8 @@ private fun FeatureFormBody( LaunchedEffect(form) { // ensure expressions are evaluated form.evaluateExpressions() - onExpressionsEvaluated() initialEvaluation = true + onExpressionsEvaluated() } } @@ -306,7 +308,7 @@ internal fun InitializingExpressions( @Composable internal fun rememberStates( form: FeatureForm, - elements : List, + elements: List, scope: CoroutineScope ): FormStateCollection { val states = MutableFormStateCollection() @@ -342,8 +344,7 @@ internal fun rememberStates( } is AttachmentFormElement -> { - val state = - rememberFakeAttachmentElementState(form = form, attachmentFormElement = element) + val state = rememberAttachmentElementState(form, element) states.add(element, state) } @@ -431,12 +432,3 @@ internal fun rememberFieldState( } } } - -@Suppress("UNUSED_PARAMETER") -@Composable -internal fun rememberFakeAttachmentElementState( - form: FeatureForm, - attachmentFormElement: AttachmentFormElement -): FakeAttachmentElementState { - return FakeAttachmentElementState(fakeAttachments) -} diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt index 1a6f0c89e..76e9c3120 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt @@ -16,15 +16,154 @@ package com.arcgismaps.toolkit.featureforms.internal.components.attachment +import android.graphics.drawable.BitmapDrawable +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.listSaver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import com.arcgismaps.LoadStatus import com.arcgismaps.mapping.featureforms.AttachmentFormElement +import com.arcgismaps.mapping.featureforms.FeatureForm +import com.arcgismaps.mapping.featureforms.FormAttachment import com.arcgismaps.toolkit.featureforms.internal.components.base.FormElementState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +/** + * Represents the state of an [AttachmentFormElement] + */ internal class AttachmentElementState( - private val formElement : AttachmentFormElement + private val formElement: AttachmentFormElement, + private val scope: CoroutineScope ) : FormElementState( label = formElement.label, description = formElement.description, isVisible = formElement.isVisible, -){ - val keyword = formElement.keyword +) { + /** + * The attachments associated with the form element. + */ + val attachments = SnapshotStateList() + + init { + scope.launch { + loadAttachments() + } + } + + /** + * Loads the attachments associated with the form element. This clears the current list of + * attachments and updates it with the list of attachments from the [formElement]. + */ + private suspend fun loadAttachments() { + formElement.fetchAttachments() + attachments.clear() + attachments.addAll( + formElement.attachments.map { + FormAttachmentState(it, scope) + } + ) + } + + companion object { + fun Saver( + attachmentFormElement: AttachmentFormElement, + scope: CoroutineScope + ): Saver = listSaver( + save = { + // save the list of indices of attachments that have been loaded + buildList { + for (i in it.attachments.indices) { + if (it.attachments[i].loadStatus.value is LoadStatus.Loaded) { + add(i) + } + } + } + }, + restore = { savedList -> + AttachmentElementState(attachmentFormElement, scope).also { + scope.launch { + it.loadAttachments() + // load the attachments that were previously loaded + savedList.forEach { index -> + it.attachments[index].loadAttachment() + } + } + } + } + ) + } +} + +/** + * Represents the state of a [FormAttachment]. + */ +internal class FormAttachmentState( + val name: String, + val size: Long, + val loadStatus: StateFlow, + private val onLoadAttachment: suspend () -> Result, + private val onLoadThumbnail: suspend () -> Result, + private val scope: CoroutineScope +) { + private val _thumbnail: MutableState = mutableStateOf(null) + val thumbnail: State = _thumbnail + + constructor(attachment: FormAttachment, scope: CoroutineScope) : this( + name = attachment.name, + size = attachment.size, + loadStatus = attachment.loadStatus, + onLoadAttachment = attachment::load, + onLoadThumbnail = attachment::createFullImage, + scope = scope + ) + + fun loadAttachment() { + scope.launch { + onLoadAttachment().onSuccess { + onLoadThumbnail().onSuccess { + if (it != null) { + _thumbnail.value = it.bitmap.asImageBitmap() + } + } + } + } + } + + companion object { + fun Saver( + attachment: FormAttachment, + scope: CoroutineScope + ): Saver = Saver( + save = {}, + restore = { + FormAttachmentState(attachment, scope) + } + ) + } +} + +@Composable +internal fun rememberAttachmentElementState( + form: FeatureForm, + attachmentFormElement: AttachmentFormElement +): AttachmentElementState { + val scope = rememberCoroutineScope() + return rememberSaveable( + inputs = arrayOf(form), + saver = AttachmentElementState.Saver(attachmentFormElement, scope) + ) { + AttachmentElementState( + formElement = attachmentFormElement, + scope = scope + ) + } } diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt index 201df7584..1e7268c68 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt @@ -16,149 +16,49 @@ package com.arcgismaps.toolkit.featureforms.internal.components.attachment -import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.spring -import androidx.compose.animation.core.updateTransition import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.Image -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Add -import androidx.compose.material.icons.rounded.AddAPhoto -import androidx.compose.material.icons.rounded.AddPhotoAlternate -import androidx.compose.material.icons.rounded.Delete -import androidx.compose.material.icons.rounded.Download -import androidx.compose.material.icons.rounded.Edit -import androidx.compose.material.icons.rounded.LibraryAdd -import androidx.compose.material.icons.rounded.MoreVert +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items import androidx.compose.material3.Card -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.composed -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.font.FontWeight.Companion.W300 -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import coil.compose.rememberAsyncImagePainter -import coil.request.ImageRequest -import com.arcgismaps.toolkit.featureforms.R -import com.arcgismaps.toolkit.featureforms.internal.components.base.FormElementState +import com.arcgismaps.LoadStatus import kotlinx.coroutines.flow.MutableStateFlow - -internal data class FakeAttachmentElementState( - val attachments: List, - val editable: Boolean = true, - val title: String = "Titanic", - val keyword: String = "point of impact",// not used - val input: String = "123",// not used - var selectedAttachment: FakeAttachment? = null -): FormElementState(label = "fake attachments", description = "fake description", isVisible = MutableStateFlow(true)) - -internal data class FakeAttachment(val name: String = "front of ship.jpg", val size: Long = 1234L) - -internal val fakeAttachments = - buildList { - repeat(40) { - add(FakeAttachment("Bow point of collision.jpeg", 1234)) - } - } - -private fun Modifier.feedbackClickable( - enabled: Boolean = true, - currentAlpha: Float = 1f, - onClick: () -> Unit = {} -) = composed { - - val source = remember { MutableInteractionSource() } - val isPressed by source.collectIsPressedAsState() - val animationTransition = updateTransition(isPressed, label = "BouncingClickableTransition") - val opacity by animationTransition.animateFloat( - targetValueByState = { pressed -> if (pressed) currentAlpha * 0.4f else currentAlpha }, - label = "ClickableOpacityTransition" - ) - - val scaleFactor by animationTransition.animateFloat( - targetValueByState = { pressed -> if (pressed) 0.8f else 1f }, - label = "ClickableScaleFactorTransition", - transitionSpec = { - spring( - dampingRatio = Spring.DampingRatioHighBouncy, - stiffness = Spring.StiffnessLow - ) - } - ) - - this - .graphicsLayer { - this.scaleX = scaleFactor - this.scaleY = scaleFactor - this.alpha = opacity - } - .clickable( - interactionSource = source, - indication = null, - enabled = enabled, - onClick = onClick - ) -} - @Composable -internal fun AttachmentFormElement(modifier: Modifier = Modifier) { +internal fun AttachmentFormElement( + state: AttachmentElementState, + modifier: Modifier = Modifier +) { AttachmentFormElement( - state = FakeAttachmentElementState(attachments = fakeAttachments, selectedAttachment = null), + label = state.label, + description = state.description, + editable = true, + attachments = state.attachments, modifier = modifier ) } -/** - * Todo: make public with a proper state object, and call from FeatureFormBody. - */ @Composable -private fun AttachmentFormElement( - state: FakeAttachmentElementState, +internal fun AttachmentFormElement( + label: String, + description: String, + editable: Boolean, + attachments: List, modifier: Modifier = Modifier, colors: AttachmentElementColors = AttachmentElementDefaults.colors() ) { @@ -168,275 +68,35 @@ private fun AttachmentFormElement( border = BorderStroke(AttachmentElementDefaults.borderThickness, colors.borderColor) ) { Column( - modifier = Modifier.padding(horizontal = 10.dp, vertical = 10.dp) + modifier = Modifier.padding(15.dp) ) { - AttachmentElementHeader( - title = state.title, - description = state.description, - editable = state.editable - ) - Spacer(modifier = Modifier.height(10.dp)) - Carousel( - onDetailsTap = { - state.selectedAttachment = it - } + Header( + title = label, + description = description, + editable = editable ) + Spacer(modifier = Modifier.height(20.dp)) + Carousel(attachments) } } } @Composable -private fun Carousel(onThumbnailTap: (FakeAttachment) -> Unit = {}, onDetailsTap: (FakeAttachment) -> Unit) { - Row( - Modifier - .horizontalScroll(rememberScrollState()) - .height(intrinsicSize = IntrinsicSize.Max), - horizontalArrangement = Arrangement.spacedBy(10.dp) +private fun Carousel(attachments: List) { + LazyRow( + horizontalArrangement = Arrangement.spacedBy(15.dp), ) { - fakeAttachments.forEach { - CarouselThumbnail( - it.name, - it.size, - onThumbnailTap = { onThumbnailTap(it) }, - onDetailsTap = { onDetailsTap(it) } - ) + items(attachments) { + AttachmentTile(it) } } } @Composable -private fun CarouselThumbnail(name: String, size: Long, onThumbnailTap: () -> Unit, onDetailsTap: () -> Unit) { - var downloaded by rememberSaveable { mutableStateOf(false) } - Column( - Modifier - .feedbackClickable { onThumbnailTap() } - .width(80.dp) - .border( - border = BorderStroke( - AttachmentElementDefaults.borderThickness, - AttachmentElementDefaults.colors().borderColor - ), - shape = RoundedCornerShape(10.dp) - ), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Box( - modifier = Modifier - .alpha(0.4f) - .aspectRatio(1.0f) - ) { - var showMenu by rememberSaveable { mutableStateOf(false) } - ThumbnailMenu(showMenu) { - showMenu = false - } - - if (downloaded) { - Image( - painter = rememberAsyncImagePainter( - ImageRequest.Builder(LocalContext.current) - .data("https://i.postimg.cc/65yws9mR/Screenshot-2024-02-02-at-6-20-49-PM.png").apply { - placeholder( - LocalContext.current.getDrawable(R.drawable.baseline_cloud_download_16) - ) - }.build() - ), - contentScale = ContentScale.Crop, - contentDescription = "Thumbnail image", - modifier = Modifier - .size(80.dp) - .clip(shape = RoundedCornerShape(15.dp, 15.dp, 0.dp, 0.dp)) - ) - Icon( - Icons.Rounded.MoreVert, - "more", - modifier = Modifier - .align(Alignment.TopEnd) - .padding(vertical = 3.dp) - .clickable { - showMenu = true - onDetailsTap() - } - ) - } else { - Icon( - Icons.Rounded.Download, - contentDescription = "Download attachment", - modifier = Modifier - .size(80.dp) - .clip(shape = RoundedCornerShape(15.dp, 15.dp, 0.dp, 0.dp)) - .clickable { downloaded = true } - - ) - } - } - - HorizontalDivider() - CarouselText(name, size) - } -} - -@Composable -private fun ThumbnailMenu(expanded: Boolean, onDismiss: () -> Unit = {}) { - MaterialTheme(shapes = MaterialTheme.shapes.copy(extraSmall = RoundedCornerShape(8.dp))) { - DropdownMenu( - expanded = expanded, - offset = DpOffset(15.dp, (-5).dp), - onDismissRequest = onDismiss - ) { - DropdownMenuItem( - text = { Text(text = "Rename") }, - trailingIcon = { - Icon( - imageVector = Icons.Rounded.Edit, - contentDescription = "Rename attachment", - modifier = Modifier.alpha(0.4f) - ) - }, - contentPadding = PaddingValues(horizontal = 3.dp), - onClick = {} - ) - HorizontalDivider(modifier = Modifier.padding(vertical = 5.dp)) - DropdownMenuItem( - text = { Text(text = "Delete") }, - trailingIcon = { - Icon( - imageVector = Icons.Rounded.Delete, - contentDescription = "Delete attachment", - modifier = Modifier.alpha(0.4f) - ) - }, - contentPadding = PaddingValues(horizontal = 3.dp), - onClick = {} - ) -// Row( -// modifier = Modifier.padding(horizontal = 3.dp), -// horizontalArrangement = Arrangement.Start, -// verticalAlignment = Alignment.CenterVertically -// ) { -// Text(text = "Delete") -// Spacer(modifier = Modifier.weight(1f)) -// Icon( -// imageVector = Icons.Rounded.Delete, -// contentDescription = "Delete attachment", -// modifier = Modifier.alpha(0.4f) -// ) -// } -// - } - } -} - -@Composable -private fun CarouselText( - name: String, - size: Long -) { - Column { - Text( - text = name, - style = MaterialTheme.typography.labelSmall.copy( - fontWeight = FontWeight.W600 - ), - textAlign = TextAlign.Center, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.padding(horizontal = 1.dp) - ) - Text( - text = "$size KB", - style = MaterialTheme.typography.labelSmall.copy( - fontWeight = W300, - fontSize = 9.sp - ), - overflow = TextOverflow.Ellipsis, - modifier = Modifier - .padding(horizontal = 1.dp) - .align(Alignment.CenterHorizontally) - - ) - Spacer(modifier = Modifier.height(5.dp)) - } -} - -@Composable -private fun AddAttachmentMenu(expanded: Boolean, onDismiss: () -> Unit = {}) { - MaterialTheme(shapes = MaterialTheme.shapes.copy(extraSmall = RoundedCornerShape(8.dp))) { - DropdownMenu( - expanded = expanded, - offset = DpOffset.Zero, - onDismissRequest = onDismiss - ) { - DropdownMenuItem( - text= { Text(text = "Take Photo") }, - trailingIcon = { - Icon( - imageVector = Icons.Rounded.AddAPhoto, - contentDescription = "Add a photo", - modifier = Modifier.alpha(0.4f) - ) - }, - onClick = {} - ) - HorizontalDivider(modifier = Modifier.padding(vertical = 5.dp)) - DropdownMenuItem( - text= { Text(text = "Add Photo From Gallery") }, - trailingIcon = { - Icon( - imageVector = Icons.Rounded.AddPhotoAlternate, - contentDescription = "Add photo from gallery", - modifier = Modifier.alpha(0.4f) - ) - }, - onClick = {} - ) - HorizontalDivider(modifier = Modifier.padding(vertical = 5.dp)) - DropdownMenuItem( - text= { Text(text = "Add File") }, - trailingIcon = { - Icon( - imageVector = Icons.Rounded.LibraryAdd, - contentDescription = "Add File", - modifier = Modifier.alpha(0.4f) - ) - }, - onClick = {} - ) - } - } - -} - - -@Composable -private fun AddAttachment() { - var showMenu by remember { mutableStateOf(false) } - - Row { - Box( - modifier = Modifier - .size(40.dp) - .feedbackClickable { - showMenu = true - } - ) { - AddAttachmentMenu(expanded = showMenu, onDismiss = { showMenu = false }) - Icon( - Icons.Rounded.Add, - contentDescription = "Add attachment", - modifier = Modifier - .align(Alignment.Center) - .size(32.dp) - ) - } - - } -} - -@Composable -private fun AttachmentElementHeader( +private fun Header( title: String, description: String, modifier: Modifier = Modifier, - showingDetails: Boolean = false, editable: Boolean = true ) { Row( @@ -460,17 +120,25 @@ private fun AttachmentElementHeader( } } Spacer(modifier = Modifier.weight(1f)) - if (editable && !showingDetails) { - AddAttachment() - } } } @Preview @Composable -private fun PreviewFormAttachmentElement() { +private fun AttachmentFormElementPreview() { AttachmentFormElement( - modifier = Modifier - .fillMaxWidth() + label = "Attachments", + description = "Add attachments", + editable = true, + attachments = listOf( + FormAttachmentState( + "Photo 1.jpg", + 2024, + MutableStateFlow(LoadStatus.Loaded), + { Result.success(Unit) }, + { Result.success(null) }, + scope = rememberCoroutineScope() + ) + ) ) } diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt new file mode 100644 index 000000000..52bdc7c4a --- /dev/null +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt @@ -0,0 +1,308 @@ +/* + * Copyright 2024 Esri + * + * 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 + * + * http://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 com.arcgismaps.toolkit.featureforms.internal.components.attachment + +import android.text.format.Formatter +import android.util.Log +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.awaitLongPressOrCancellation +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ArrowDownward +import androidx.compose.material.icons.outlined.AudioFile +import androidx.compose.material.icons.outlined.ErrorOutline +import androidx.compose.material.icons.outlined.FileCopy +import androidx.compose.material.icons.outlined.FilePresent +import androidx.compose.material.icons.outlined.Image +import androidx.compose.material.icons.outlined.VideoCameraBack +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.arcgismaps.LoadStatus +import kotlinx.coroutines.flow.MutableStateFlow + +@Composable +internal fun AttachmentTile( + state: FormAttachmentState +) { + val loadStatus by state.loadStatus.collectAsState() + val thumbnail by state.thumbnail + Log.e("TAG", "AttachmentTile ${state.name}: $loadStatus", ) + Box( + modifier = Modifier + .width(92.dp) + .height(75.dp) + .clip(shape = RoundedCornerShape(8.dp)) + .border( + border = BorderStroke(0.5.dp, MaterialTheme.colorScheme.outline), + shape = RoundedCornerShape(8.dp) + ) + .pointerInput(Unit) { + awaitEachGesture { + val down = awaitFirstDown(requireUnconsumed = false) + awaitLongPressOrCancellation(down.id)?.let { + // handle long press + } + } + } + .clickable { + if (loadStatus is LoadStatus.NotLoaded || loadStatus is LoadStatus.FailedToLoad) { + // load attachment + state.loadAttachment() + } else if (loadStatus is LoadStatus.Loaded) { + // open attachment + } + } + ) { + when (loadStatus) { + LoadStatus.Loaded -> LoadedView( + thumbnail = thumbnail, + title = state.name + ) + + LoadStatus.Loading -> DefaultView( + title = state.name, + size = state.size, + isLoading = true, + isError = false + ) + + LoadStatus.NotLoaded -> DefaultView( + title = state.name, + size = state.size, + isLoading = false, + isError = false + ) + + is LoadStatus.FailedToLoad -> DefaultView( + title = state.name, + size = state.size, + isLoading = false, + isError = true + ) + } + } +} + +@Composable +private fun LoadedView( + thumbnail: ImageBitmap?, + title: String, + modifier: Modifier = Modifier +) { + val attachmentType = remember(title) { + getAttachmentType(title) + } + Box( + modifier = modifier + .fillMaxSize() + ) { + if (thumbnail != null) { + Image( + bitmap = thumbnail, + contentDescription = null, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop + ) + } else { + Icon( + imageVector = attachmentType.getIcon(), + contentDescription = null, + modifier = Modifier + .fillMaxSize() + .padding(top = 10.dp, bottom = 25.dp) + .align(Alignment.Center) + ) + } + Column( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .height(20.dp) + .background( + MaterialTheme.colorScheme.onBackground.copy( + alpha = 0.7f + ) + ), + verticalArrangement = Arrangement.Center + ) { + Title( + text = title, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 5.dp), + color = MaterialTheme.colorScheme.background + ) + } + } +} + +@Composable +private fun DefaultView( + title: String, + size: Long, + isLoading: Boolean, + isError: Boolean, + modifier: Modifier = Modifier, +) { + val attachmentType = remember(title) { + getAttachmentType(title) + } + Column( + modifier = modifier + .fillMaxSize() + .padding(horizontal = 5.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceEvenly + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Size(size = size) + Icon( + imageVector = Icons.Outlined.ArrowDownward, + contentDescription = null, + modifier = Modifier.size(11.dp) + ) + } + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + strokeWidth = 2.dp + ) + } else if (isError) { + Image( + imageVector = Icons.Outlined.ErrorOutline, + contentDescription = null, + modifier = Modifier.size(20.dp), + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.error) + ) + } else { + Icon( + imageVector = attachmentType.getIcon(), + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + } + Title(text = title, modifier = Modifier) + } +} + +@Composable +private fun Title( + text: String, + modifier: Modifier = Modifier, + color: Color = Color.Unspecified, + style: TextStyle = MaterialTheme.typography.labelSmall +) { + Text( + text = text, + color = color, + style = style, + textAlign = TextAlign.Center, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + modifier = modifier.padding(horizontal = 1.dp) + ) +} + +@Composable +private fun Size( + size: Long, modifier: + Modifier = Modifier +) { + val context = LocalContext.current + val fileSize = Formatter.formatFileSize(context, size) + Text( + text = fileSize, + style = MaterialTheme.typography.labelSmall.copy( + fontWeight = FontWeight.W300, + fontSize = 9.sp + ), + overflow = TextOverflow.Ellipsis, + modifier = modifier + .padding(horizontal = 1.dp) + ) +} + +private sealed class AttachmentType { + data object Image : AttachmentType() + data object Audio : AttachmentType() + data object Video : AttachmentType() + data object Document : AttachmentType() + data object Other : AttachmentType() +} + +private fun getAttachmentType(filename: String): AttachmentType { + val extension = filename.substring(filename.lastIndexOf(".") + 1) + return when (extension) { + "jpg", "jpeg", "png", "gif", "bmp" -> AttachmentType.Image + "mp3", "wav", "ogg", "flac" -> AttachmentType.Audio + "mp4", "avi", "mov", "wmv", "flv" -> AttachmentType.Video + "doc", "docx", "pdf", "txt", "rtf" -> AttachmentType.Document + else -> AttachmentType.Other + } +} + +@Composable +private fun AttachmentType.getIcon(): ImageVector = when (this) { + AttachmentType.Image -> Icons.Outlined.Image + AttachmentType.Audio -> Icons.Outlined.AudioFile + AttachmentType.Video -> Icons.Outlined.VideoCameraBack + AttachmentType.Document -> Icons.Outlined.FilePresent + AttachmentType.Other -> Icons.Outlined.FileCopy +} From ed60c3581311da81ac5130b1f93a73d8e5c53c2c Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Mon, 29 Apr 2024 16:15:35 -0700 Subject: [PATCH 03/32] revert un-needed changes --- .../java/com/arcgismaps/toolkit/featureforms/FeatureForm.kt | 6 ------ .../internal/components/text/FormTextFieldState.kt | 3 --- 2 files changed, 9 deletions(-) diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/FeatureForm.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/FeatureForm.kt index c3c74b226..41251a79b 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/FeatureForm.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/FeatureForm.kt @@ -18,7 +18,6 @@ package com.arcgismaps.toolkit.featureforms -import android.util.Log import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -39,14 +38,11 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.neverEqualPolicy import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer @@ -65,7 +61,6 @@ import com.arcgismaps.mapping.featureforms.RadioButtonsFormInput import com.arcgismaps.mapping.featureforms.SwitchFormInput import com.arcgismaps.mapping.featureforms.TextAreaFormInput import com.arcgismaps.mapping.featureforms.TextBoxFormInput -import com.arcgismaps.toolkit.featureforms.internal.components.attachment.AttachmentElementState import com.arcgismaps.toolkit.featureforms.internal.components.attachment.AttachmentFormElement import com.arcgismaps.toolkit.featureforms.internal.components.attachment.rememberAttachmentElementState import com.arcgismaps.toolkit.featureforms.internal.components.base.BaseFieldState @@ -83,7 +78,6 @@ import com.arcgismaps.toolkit.featureforms.internal.components.formelement.Group import com.arcgismaps.toolkit.featureforms.internal.components.text.rememberFormTextFieldState import com.arcgismaps.toolkit.featureforms.internal.utils.FeatureFormDialog import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay /** * The "property" determines the behavior of when the validation errors are visible. diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/text/FormTextFieldState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/text/FormTextFieldState.kt index dd8ff4a64..bac2577d6 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/text/FormTextFieldState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/text/FormTextFieldState.kt @@ -16,7 +16,6 @@ package com.arcgismaps.toolkit.featureforms.internal.components.text -import android.util.Log import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.saveable.Saver @@ -37,7 +36,6 @@ import com.arcgismaps.toolkit.featureforms.internal.components.base.formattedVal import com.arcgismaps.toolkit.featureforms.internal.components.base.mapValidationErrors import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch internal class TextFieldProperties( label: String, @@ -173,7 +171,6 @@ internal fun rememberFormTextFieldState( inputs = arrayOf(form), saver = FormTextFieldState.Saver(field, form, scope) ) { - Log.e("TAG", "rememberFormTextFieldState: init for ${field.label}", ) FormTextFieldState( properties = TextFieldProperties( label = field.label, From 3f526a2041c22aada30d52b392ced8caa8f3c731 Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Mon, 29 Apr 2024 16:17:31 -0700 Subject: [PATCH 04/32] remove saver --- .../components/attachment/AttachmentElementState.kt | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt index 76e9c3120..60c0ed96b 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt @@ -137,18 +137,6 @@ internal class FormAttachmentState( } } } - - companion object { - fun Saver( - attachment: FormAttachment, - scope: CoroutineScope - ): Saver = Saver( - save = {}, - restore = { - FormAttachmentState(attachment, scope) - } - ) - } } @Composable From 880b8dd96033d607c43132f2e533fe46fee8ae47 Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Mon, 29 Apr 2024 16:18:58 -0700 Subject: [PATCH 05/32] Update AttachmentTile.kt --- .../internal/components/attachment/AttachmentTile.kt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt index 52bdc7c4a..ec0349bef 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt @@ -17,7 +17,6 @@ package com.arcgismaps.toolkit.featureforms.internal.components.attachment import android.text.format.Formatter -import android.util.Log import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -53,7 +52,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -68,11 +66,9 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.arcgismaps.LoadStatus -import kotlinx.coroutines.flow.MutableStateFlow @Composable internal fun AttachmentTile( @@ -80,7 +76,6 @@ internal fun AttachmentTile( ) { val loadStatus by state.loadStatus.collectAsState() val thumbnail by state.thumbnail - Log.e("TAG", "AttachmentTile ${state.name}: $loadStatus", ) Box( modifier = Modifier .width(92.dp) From e8bfb1f10602c6ec551eed5813cb7682541b1b6f Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Tue, 30 Apr 2024 12:33:30 -0700 Subject: [PATCH 06/32] init add attachments work --- .../featureforms/src/main/AndroidManifest.xml | 13 ++ .../attachment/AttachmentElementState.kt | 56 +++++ .../attachment/AttachmentFormElement.kt | 204 ++++++++++++++++-- .../components/attachment/AttachmentTile.kt | 28 --- .../utils/AttachmentCaptureFileProvider.kt | 43 ++++ .../featureforms/internal/utils/Dialog.kt | 12 ++ .../feature_forms_captured_attachments.xml | 23 ++ 7 files changed, 339 insertions(+), 40 deletions(-) create mode 100644 toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/AttachmentCaptureFileProvider.kt create mode 100644 toolkit/featureforms/src/main/res/xml/feature_forms_captured_attachments.xml diff --git a/toolkit/featureforms/src/main/AndroidManifest.xml b/toolkit/featureforms/src/main/AndroidManifest.xml index 4e1ed5d7e..eb3a2c57f 100644 --- a/toolkit/featureforms/src/main/AndroidManifest.xml +++ b/toolkit/featureforms/src/main/AndroidManifest.xml @@ -16,4 +16,17 @@ --> + + + + + + + diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt index 60c0ed96b..e4485160a 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt @@ -17,6 +17,13 @@ package com.arcgismaps.toolkit.featureforms.internal.components.attachment import android.graphics.drawable.BitmapDrawable +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.AudioFile +import androidx.compose.material.icons.outlined.FileCopy +import androidx.compose.material.icons.outlined.FilePresent +import androidx.compose.material.icons.outlined.Image +import androidx.compose.material.icons.outlined.VideoCameraBack import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.State @@ -28,6 +35,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.vector.ImageVector import com.arcgismaps.LoadStatus import com.arcgismaps.mapping.featureforms.AttachmentFormElement import com.arcgismaps.mapping.featureforms.FeatureForm @@ -53,6 +61,11 @@ internal class AttachmentElementState( */ val attachments = SnapshotStateList() + /** + * The state of the lazy list that displays the [attachments]. + */ + val lazyListState = LazyListState() + init { scope.launch { loadAttachments() @@ -73,6 +86,19 @@ internal class AttachmentElementState( ) } + /** + * Adds an attachment with the given [name], [contentType], and [data]. + */ + suspend fun addAttachment(name: String, contentType: String, data: ByteArray) { + formElement.addAttachment(name, contentType, data) + // refresh the list of attachments + loadAttachments() + // load the attachment that was just added + attachments.last().loadAttachment() + // scroll to the newly added attachment + lazyListState.scrollToItem(attachments.size - 1) + } + companion object { fun Saver( attachmentFormElement: AttachmentFormElement, @@ -117,6 +143,8 @@ internal class FormAttachmentState( private val _thumbnail: MutableState = mutableStateOf(null) val thumbnail: State = _thumbnail + val type: AttachmentType = getAttachmentType(name) + constructor(attachment: FormAttachment, scope: CoroutineScope) : this( name = attachment.name, size = attachment.size, @@ -155,3 +183,31 @@ internal fun rememberAttachmentElementState( ) } } + +internal sealed class AttachmentType { + data object Image : AttachmentType() + data object Audio : AttachmentType() + data object Video : AttachmentType() + data object Document : AttachmentType() + data object Other : AttachmentType() +} + +internal fun getAttachmentType(filename: String): AttachmentType { + val extension = filename.substring(filename.lastIndexOf(".") + 1) + return when (extension) { + "jpg", "jpeg", "png", "gif", "bmp" -> AttachmentType.Image + "mp3", "wav", "ogg", "flac" -> AttachmentType.Audio + "mp4", "avi", "mov", "wmv", "flv" -> AttachmentType.Video + "doc", "docx", "pdf", "txt", "rtf" -> AttachmentType.Document + else -> AttachmentType.Other + } +} + +@Composable +internal fun AttachmentType.getIcon(): ImageVector = when (this) { + AttachmentType.Image -> Icons.Outlined.Image + AttachmentType.Audio -> Icons.Outlined.AudioFile + AttachmentType.Video -> Icons.Outlined.VideoCameraBack + AttachmentType.Document -> Icons.Outlined.FilePresent + AttachmentType.Other -> Icons.Outlined.FileCopy +} diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt index 1e7268c68..f5b40d12f 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt @@ -16,39 +16,79 @@ package com.arcgismaps.toolkit.featureforms.internal.components.attachment +import android.content.Context +import android.net.Uri +import android.util.Log +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Add +import androidx.compose.material.icons.rounded.PhotoCamera import androidx.compose.material3.Card +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.listSaver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import com.arcgismaps.LoadStatus +import com.arcgismaps.toolkit.featureforms.internal.utils.AttachmentCaptureFileProvider +import com.arcgismaps.toolkit.featureforms.internal.utils.DialogType +import com.arcgismaps.toolkit.featureforms.internal.utils.LocalDialogRequester +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import java.io.File +import java.time.Instant @Composable internal fun AttachmentFormElement( state: AttachmentElementState, modifier: Modifier = Modifier ) { + val scope = rememberCoroutineScope() AttachmentFormElement( label = state.label, description = state.description, editable = true, attachments = state.attachments, + lazyListState = state.lazyListState, + onAttachmentAdded = { name, contentType, data -> + scope.launch { + state.addAttachment(name, contentType, data) + } + }, modifier = modifier ) } @@ -59,9 +99,13 @@ internal fun AttachmentFormElement( description: String, editable: Boolean, attachments: List, + lazyListState: LazyListState, + onAttachmentAdded: suspend (String, String, ByteArray) -> Unit, modifier: Modifier = Modifier, colors: AttachmentElementColors = AttachmentElementDefaults.colors() ) { + val context = LocalContext.current + val scope = rememberCoroutineScope() Card( modifier = modifier, shape = AttachmentElementDefaults.containerShape, @@ -70,23 +114,41 @@ internal fun AttachmentFormElement( Column( modifier = Modifier.padding(15.dp) ) { - Header( - title = label, - description = description, - editable = editable - ) + Row { + Header( + title = label, + description = description + ) + Spacer(modifier = Modifier.weight(1f)) + if (editable) { + // Add attachment button + AddAttachment { contentType, uri -> + scope.launch(Dispatchers.IO) { + context.readBytes(uri)?.let { + val name = attachments.getNewAttachmentNameForContentType( + contentType, + uri + ) + Log.e("TAG", "AttachmentFormElement: $name, $contentType, $uri", ) + onAttachmentAdded(name, contentType, it) + } + } + } + } + } Spacer(modifier = Modifier.height(20.dp)) - Carousel(attachments) + Carousel(lazyListState, attachments) } } } @Composable -private fun Carousel(attachments: List) { +private fun Carousel(state : LazyListState, attachments: List) { LazyRow( + state = state, horizontalArrangement = Arrangement.spacedBy(15.dp), ) { - items(attachments) { + items(attachments, key = { it.name + it.type + it.size }) { AttachmentTile(it) } } @@ -96,8 +158,7 @@ private fun Carousel(attachments: List) { private fun Header( title: String, description: String, - modifier: Modifier = Modifier, - editable: Boolean = true + modifier: Modifier = Modifier ) { Row( modifier = modifier.wrapContentHeight(), @@ -119,10 +180,127 @@ private fun Header( ) } } - Spacer(modifier = Modifier.weight(1f)) } } +@Composable +private fun AddAttachment(onAttachment: (String, Uri) -> Unit) { + var showMenu by remember { mutableStateOf(false) } + val dialogRequester = LocalDialogRequester.current + val scope = rememberCoroutineScope() + val pickerStyle = remember { MutableSharedFlow() } + val context = LocalContext.current + Box { + IconButton( + onClick = { showMenu = true }, + ) { + Icon( + Icons.Rounded.Add, + contentDescription = "Add attachment", + modifier = Modifier.size(32.dp) + ) + } + DropdownMenu( + expanded = showMenu, + offset = DpOffset.Zero, + onDismissRequest = { showMenu = false } + ) { + DropdownMenuItem( + text = { Text(text = "Take Photo") }, + trailingIcon = { + Icon( + imageVector = Icons.Rounded.PhotoCamera, + contentDescription = "Take Photo", + modifier = Modifier.alpha(0.4f) + ) + }, + onClick = { + scope.launch { + pickerStyle.emit(PickerStyle.Camera) + showMenu = false + } + } + ) + } + } + LaunchedEffect(Unit) { + pickerStyle.collect { + when (it) { + PickerStyle.Camera -> { + dialogRequester.requestDialog(DialogType.ImagePickerDialog { uri -> + onAttachment("image/jpeg", uri) + }) + } + + PickerStyle.Gallery -> {} + PickerStyle.None -> {} + } + } + } +} + +@Composable +internal fun ImagePicker(onImageCaptured: (Uri) -> Unit) { + val context = LocalContext.current + var hasLaunched by rememberSaveable { + mutableStateOf(false) + } + val capturedImageUri = rememberSaveable( + saver = listSaver( + save = { listOf(it.toString()) }, + restore = { Uri.parse(it.first()) } + ) + ) { + val file = context.createImageFile() + AttachmentCaptureFileProvider.getImageUri(file, context) + } + val cameraLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.TakePicture(), + onResult = { success -> + if (success) { + onImageCaptured(capturedImageUri) + } + } + ) + LaunchedEffect(Unit) { + if (!hasLaunched) { + hasLaunched = true + cameraLauncher.launch(capturedImageUri) + } + } +} + +private fun List.getNewAttachmentNameForContentType( + contentType: String, + uri: Uri +): String { + val attachmentType : AttachmentType = when (contentType) { + "image/jpeg" -> AttachmentType.Image + else -> AttachmentType.Other + } + val count = this.count { it.type == attachmentType } + return "$attachmentType $count" +} + +private fun Context.createImageFile(): File { + val timeStamp = Instant.now().toEpochMilli() + val dir = File(cacheDir, "feature_forms_attachments") + return File.createTempFile( + "IMAGE_$timeStamp", + ".jpg", + dir, + ) +} + +private fun Context.readBytes(uri: Uri): ByteArray? = + contentResolver.openInputStream(uri)?.use { it.buffered().readBytes() } + +private sealed class PickerStyle { + data object None : PickerStyle() + data object Camera : PickerStyle() + data object Gallery : PickerStyle() +} + @Preview @Composable private fun AttachmentFormElementPreview() { @@ -139,6 +317,8 @@ private fun AttachmentFormElementPreview() { { Result.success(null) }, scope = rememberCoroutineScope() ) - ) + ), + LazyListState(), + { _, _, _ -> } ) } diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt index ec0349bef..a6fe29903 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt @@ -273,31 +273,3 @@ private fun Size( .padding(horizontal = 1.dp) ) } - -private sealed class AttachmentType { - data object Image : AttachmentType() - data object Audio : AttachmentType() - data object Video : AttachmentType() - data object Document : AttachmentType() - data object Other : AttachmentType() -} - -private fun getAttachmentType(filename: String): AttachmentType { - val extension = filename.substring(filename.lastIndexOf(".") + 1) - return when (extension) { - "jpg", "jpeg", "png", "gif", "bmp" -> AttachmentType.Image - "mp3", "wav", "ogg", "flac" -> AttachmentType.Audio - "mp4", "avi", "mov", "wmv", "flv" -> AttachmentType.Video - "doc", "docx", "pdf", "txt", "rtf" -> AttachmentType.Document - else -> AttachmentType.Other - } -} - -@Composable -private fun AttachmentType.getIcon(): ImageVector = when (this) { - AttachmentType.Image -> Icons.Outlined.Image - AttachmentType.Audio -> Icons.Outlined.AudioFile - AttachmentType.Video -> Icons.Outlined.VideoCameraBack - AttachmentType.Document -> Icons.Outlined.FilePresent - AttachmentType.Other -> Icons.Outlined.FileCopy -} diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/AttachmentCaptureFileProvider.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/AttachmentCaptureFileProvider.kt new file mode 100644 index 000000000..42b592948 --- /dev/null +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/AttachmentCaptureFileProvider.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2024 Esri + * + * 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 + * + * http://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 com.arcgismaps.toolkit.featureforms.internal.utils + +import android.content.Context +import android.net.Uri +import androidx.core.content.FileProvider +import com.arcgismaps.toolkit.featureforms.R +import java.io.File + +internal class AttachmentCaptureFileProvider : + FileProvider(R.xml.feature_forms_captured_attachments) { + companion object { + private const val AUTHORITY = "com.arcgismaps.toolkit.featureforms.capturefileprovider" + + fun getImageUri(file: File, context: Context): Uri { + val directory = File(context.cacheDir, "feature_forms_attachments") + directory.mkdirs() + // The authority string must be unique per device. Therefore use of this provider with two + // installations of the featureforms dependency on one device will crash. + // The solution is to release a standalone file provider aidl service which both instances can use. + return getUriForFile( + context, + AUTHORITY, + file, + ) + } + } +} diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt index 90286dd68..e0f461593 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt @@ -17,6 +17,7 @@ package com.arcgismaps.toolkit.featureforms.internal.utils import android.content.Context +import android.net.Uri import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.collectAsState @@ -29,6 +30,7 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.window.core.layout.WindowSizeClass import androidx.window.layout.WindowMetricsCalculator import com.arcgismaps.toolkit.featureforms.R +import com.arcgismaps.toolkit.featureforms.internal.components.attachment.ImagePicker import com.arcgismaps.toolkit.featureforms.internal.components.codedvalue.CodedValueFieldState import com.arcgismaps.toolkit.featureforms.internal.components.codedvalue.ComboBoxDialog import com.arcgismaps.toolkit.featureforms.internal.components.datetime.DateTimeFieldState @@ -92,6 +94,8 @@ internal sealed class DialogType { * @param state The [DateTimeFieldState] to use for the dialog. */ data class DateTimeDialog(val state: DateTimeFieldState) : DialogType() + + data class ImagePickerDialog(val onImage: (Uri) -> Unit) : DialogType() } /** @@ -159,6 +163,14 @@ internal fun FeatureFormDialog() { ) } + is DialogType.ImagePickerDialog -> { + val onImage = (dialogType as DialogType.ImagePickerDialog).onImage + ImagePicker { + onImage(it) + dialogRequester.dismissDialog() + } + } + else -> { // clear focus from the originating tapped field if (dialogType == null) { diff --git a/toolkit/featureforms/src/main/res/xml/feature_forms_captured_attachments.xml b/toolkit/featureforms/src/main/res/xml/feature_forms_captured_attachments.xml new file mode 100644 index 000000000..a87b5ada1 --- /dev/null +++ b/toolkit/featureforms/src/main/res/xml/feature_forms_captured_attachments.xml @@ -0,0 +1,23 @@ + + + + + From 9b6d8b2b362dc1198bdf548d6998877ffa639ce9 Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Wed, 1 May 2024 14:12:53 -0700 Subject: [PATCH 07/32] added permissions for camera access --- .../toolkit/featureformsapp/MainActivity.kt | 65 ++++++++++++- .../app/src/main/res/values/strings.xml | 1 + .../attachment/AttachmentElementState.kt | 11 +++ .../attachment/AttachmentFormElement.kt | 93 +++++++++++++++---- .../featureforms/internal/utils/Dialog.kt | 26 +++++- 5 files changed, 169 insertions(+), 27 deletions(-) diff --git a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/MainActivity.kt b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/MainActivity.kt index aa4d401c6..87b3b8521 100644 --- a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/MainActivity.kt +++ b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/MainActivity.kt @@ -18,24 +18,38 @@ package com.arcgismaps.toolkit.featureformsapp +import android.Manifest.permission.CAMERA +import android.content.pm.PackageManager import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Warning +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope import androidx.navigation.compose.rememberNavController import com.arcgismaps.ArcGISEnvironment @@ -73,12 +87,26 @@ class MainActivity : ComponentActivity() { private val appState: MutableStateFlow = MutableStateFlow(AppState.Loading) + private val hasPermissions = mutableStateOf(null) + + // Register the permissions callback, which handles the user's response to the + // system permissions dialog. + private val requestPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted: Boolean -> + hasPermissions.value = isGranted + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ArcGISEnvironment.applicationContext = this setContent { FeatureFormsAppTheme { - FeatureFormApp(appState.collectAsState().value, navigator) + FeatureFormApp( + appState.collectAsState().value, + navigator, + hasPermissions.value + ) } } lifecycleScope.launch { @@ -89,6 +117,14 @@ class MainActivity : ComponentActivity() { ) loadCredentials(factory.getPortalSettings()) } + // check for permissions + when (ContextCompat.checkSelfPermission(this, CAMERA)) { + PackageManager.PERMISSION_GRANTED -> { + hasPermissions.value = true + } + + else -> requestPermissionLauncher.launch(CAMERA) + } } private suspend fun loadCredentials(portalSettings: PortalSettings) = @@ -118,7 +154,14 @@ class MainActivity : ComponentActivity() { } @Composable -fun FeatureFormApp(appState: AppState, navigator: Navigator) { +fun FeatureFormApp( + appState: AppState, + navigator: Navigator, + hasPermissions: Boolean? +) { + var showPermissionsDialog by remember(hasPermissions) { + mutableStateOf(hasPermissions != null && hasPermissions == false) + } if (appState is AppState.Loading) { AnimatedLoading({ true }, modifier = Modifier.fillMaxSize()) } else { @@ -138,6 +181,24 @@ fun FeatureFormApp(appState: AppState, navigator: Navigator) { startDestination = startDestination ) } + if (showPermissionsDialog) { + AlertDialog( + onDismissRequest = { + showPermissionsDialog = false + }, + text = { + Text(text = stringResource(R.string.camera_permission_required)) + }, + icon = { + Icon(imageVector = Icons.Rounded.Warning, contentDescription = "Warning") + }, + confirmButton = { + Button(onClick = { showPermissionsDialog = false }) { + Text(text = stringResource(id = R.string.okay)) + } + } + ) + } } @Composable diff --git a/microapps/FeatureFormsApp/app/src/main/res/values/strings.xml b/microapps/FeatureFormsApp/app/src/main/res/values/strings.xml index 0098e9d80..dd15d8f08 100644 --- a/microapps/FeatureFormsApp/app/src/main/res/values/strings.xml +++ b/microapps/FeatureFormsApp/app/src/main/res/values/strings.xml @@ -47,4 +47,5 @@ Okay Exit FeatureLayers in this map do not contain a FeatureFormDefinition. Feature editing will be disabled. + Camera permission is required for Attachments. This will result in limited functionality. diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt index e4485160a..6ea9494ec 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt @@ -16,6 +16,9 @@ package com.arcgismaps.toolkit.featureforms.internal.components.attachment +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager import android.graphics.drawable.BitmapDrawable import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material.icons.Icons @@ -36,6 +39,7 @@ import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.vector.ImageVector +import androidx.core.content.ContextCompat import com.arcgismaps.LoadStatus import com.arcgismaps.mapping.featureforms.AttachmentFormElement import com.arcgismaps.mapping.featureforms.FeatureForm @@ -66,6 +70,8 @@ internal class AttachmentElementState( */ val lazyListState = LazyListState() + val inputType = formElement.input + init { scope.launch { loadAttachments() @@ -99,6 +105,11 @@ internal class AttachmentElementState( lazyListState.scrollToItem(attachments.size - 1) } + fun hasCameraPermissions(context: Context): Boolean = ContextCompat.checkSelfPermission( + context, + Manifest.permission.CAMERA + ) == PackageManager.PERMISSION_GRANTED + companion object { fun Saver( attachmentFormElement: AttachmentFormElement, diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt index f5b40d12f..c1cbde89d 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt @@ -20,6 +20,7 @@ import android.content.Context import android.net.Uri import android.util.Log import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement @@ -61,6 +62,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp +import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType +import androidx.compose.material.icons.rounded.Photo import com.arcgismaps.LoadStatus import com.arcgismaps.toolkit.featureforms.internal.utils.AttachmentCaptureFileProvider import com.arcgismaps.toolkit.featureforms.internal.utils.DialogType @@ -78,12 +81,14 @@ internal fun AttachmentFormElement( modifier: Modifier = Modifier ) { val scope = rememberCoroutineScope() + val context = LocalContext.current AttachmentFormElement( label = state.label, description = state.description, editable = true, attachments = state.attachments, lazyListState = state.lazyListState, + hasCameraPermission = state.hasCameraPermissions(context), onAttachmentAdded = { name, contentType, data -> scope.launch { state.addAttachment(name, contentType, data) @@ -100,6 +105,7 @@ internal fun AttachmentFormElement( editable: Boolean, attachments: List, lazyListState: LazyListState, + hasCameraPermission: Boolean, onAttachmentAdded: suspend (String, String, ByteArray) -> Unit, modifier: Modifier = Modifier, colors: AttachmentElementColors = AttachmentElementDefaults.colors() @@ -122,14 +128,16 @@ internal fun AttachmentFormElement( Spacer(modifier = Modifier.weight(1f)) if (editable) { // Add attachment button - AddAttachment { contentType, uri -> + AddAttachment( + hasCameraPermission = hasCameraPermission + ) { contentType, uri -> scope.launch(Dispatchers.IO) { context.readBytes(uri)?.let { val name = attachments.getNewAttachmentNameForContentType( contentType, uri ) - Log.e("TAG", "AttachmentFormElement: $name, $contentType, $uri", ) + Log.e("TAG", "AttachmentFormElement: $name, $contentType, $uri") onAttachmentAdded(name, contentType, it) } } @@ -143,7 +151,7 @@ internal fun AttachmentFormElement( } @Composable -private fun Carousel(state : LazyListState, attachments: List) { +private fun Carousel(state: LazyListState, attachments: List) { LazyRow( state = state, horizontalArrangement = Arrangement.spacedBy(15.dp), @@ -184,12 +192,14 @@ private fun Header( } @Composable -private fun AddAttachment(onAttachment: (String, Uri) -> Unit) { +private fun AddAttachment( + hasCameraPermission : Boolean, + onAttachment: (String, Uri) -> Unit +) { var showMenu by remember { mutableStateOf(false) } val dialogRequester = LocalDialogRequester.current val scope = rememberCoroutineScope() val pickerStyle = remember { MutableSharedFlow() } - val context = LocalContext.current Box { IconButton( onClick = { showMenu = true }, @@ -205,18 +215,36 @@ private fun AddAttachment(onAttachment: (String, Uri) -> Unit) { offset = DpOffset.Zero, onDismissRequest = { showMenu = false } ) { + if (hasCameraPermission) { + DropdownMenuItem( + text = { Text(text = "Take Photo") }, + trailingIcon = { + Icon( + imageVector = Icons.Rounded.PhotoCamera, + contentDescription = "Take Photo", + modifier = Modifier.alpha(0.4f) + ) + }, + onClick = { + scope.launch { + pickerStyle.emit(PickerStyle.Camera) + showMenu = false + } + } + ) + } DropdownMenuItem( - text = { Text(text = "Take Photo") }, + text = { Text(text = "Add Photo") }, trailingIcon = { Icon( - imageVector = Icons.Rounded.PhotoCamera, - contentDescription = "Take Photo", + imageVector = Icons.Rounded.Photo, + contentDescription = "Add Photo", modifier = Modifier.alpha(0.4f) ) }, onClick = { scope.launch { - pickerStyle.emit(PickerStyle.Camera) + pickerStyle.emit(PickerStyle.Gallery) showMenu = false } } @@ -227,20 +255,29 @@ private fun AddAttachment(onAttachment: (String, Uri) -> Unit) { pickerStyle.collect { when (it) { PickerStyle.Camera -> { - dialogRequester.requestDialog(DialogType.ImagePickerDialog { uri -> + dialogRequester.requestDialog(DialogType.ImageCaptureDialog { uri -> onAttachment("image/jpeg", uri) }) } - PickerStyle.Gallery -> {} - PickerStyle.None -> {} + PickerStyle.Gallery -> { + dialogRequester.requestDialog( + DialogType.GalleryPickerDialog( + ActivityResultContracts.PickVisualMedia.ImageOnly + ) { uri -> + onAttachment("image/jpeg", uri) + } + ) + } + + else -> {} } } } } @Composable -internal fun ImagePicker(onImageCaptured: (Uri) -> Unit) { +internal fun ImageCapture(onImageCaptured: (Uri) -> Unit) { val context = LocalContext.current var hasLaunched by rememberSaveable { mutableStateOf(false) @@ -270,21 +307,36 @@ internal fun ImagePicker(onImageCaptured: (Uri) -> Unit) { } } +@Composable +internal fun GalleryPicker(visualMediaType: VisualMediaType, onImageSelected: (Uri) -> Unit) { + val launcher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.PickVisualMedia() + ) { + if (it != null) { + onImageSelected(it) + } + } + LaunchedEffect(Unit) { + launcher.launch(PickVisualMediaRequest(visualMediaType)) + } +} + private fun List.getNewAttachmentNameForContentType( contentType: String, uri: Uri ): String { - val attachmentType : AttachmentType = when (contentType) { - "image/jpeg" -> AttachmentType.Image - else -> AttachmentType.Other + val (attachmentType: AttachmentType, ext: String) = when (contentType) { + "image/jpeg" -> Pair(AttachmentType.Image, "jpg") + else -> Pair(AttachmentType.Other, "") } val count = this.count { it.type == attachmentType } - return "$attachmentType $count" + return "$attachmentType $count.$ext" } private fun Context.createImageFile(): File { val timeStamp = Instant.now().toEpochMilli() val dir = File(cacheDir, "feature_forms_attachments") + dir.mkdirs() return File.createTempFile( "IMAGE_$timeStamp", ".jpg", @@ -296,7 +348,7 @@ private fun Context.readBytes(uri: Uri): ByteArray? = contentResolver.openInputStream(uri)?.use { it.buffered().readBytes() } private sealed class PickerStyle { - data object None : PickerStyle() + data object File : PickerStyle() data object Camera : PickerStyle() data object Gallery : PickerStyle() } @@ -318,7 +370,8 @@ private fun AttachmentFormElementPreview() { scope = rememberCoroutineScope() ) ), - LazyListState(), - { _, _, _ -> } + lazyListState = LazyListState(), + hasCameraPermission = true, + onAttachmentAdded = { _, _, _ -> } ) } diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt index e0f461593..a6adbe1f8 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt @@ -29,8 +29,10 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.window.core.layout.WindowSizeClass import androidx.window.layout.WindowMetricsCalculator +import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType import com.arcgismaps.toolkit.featureforms.R -import com.arcgismaps.toolkit.featureforms.internal.components.attachment.ImagePicker +import com.arcgismaps.toolkit.featureforms.internal.components.attachment.GalleryPicker +import com.arcgismaps.toolkit.featureforms.internal.components.attachment.ImageCapture import com.arcgismaps.toolkit.featureforms.internal.components.codedvalue.CodedValueFieldState import com.arcgismaps.toolkit.featureforms.internal.components.codedvalue.ComboBoxDialog import com.arcgismaps.toolkit.featureforms.internal.components.datetime.DateTimeFieldState @@ -95,7 +97,12 @@ internal sealed class DialogType { */ data class DateTimeDialog(val state: DateTimeFieldState) : DialogType() - data class ImagePickerDialog(val onImage: (Uri) -> Unit) : DialogType() + data class ImageCaptureDialog(val onImage: (Uri) -> Unit) : DialogType() + + data class GalleryPickerDialog( + val visualMediaType: VisualMediaType, + val onPick: (Uri) -> Unit + ) : DialogType() } /** @@ -163,14 +170,23 @@ internal fun FeatureFormDialog() { ) } - is DialogType.ImagePickerDialog -> { - val onImage = (dialogType as DialogType.ImagePickerDialog).onImage - ImagePicker { + is DialogType.ImageCaptureDialog -> { + val onImage = (dialogType as DialogType.ImageCaptureDialog).onImage + ImageCapture { onImage(it) dialogRequester.dismissDialog() } } + is DialogType.GalleryPickerDialog -> { + val visualMediaType = (dialogType as DialogType.GalleryPickerDialog).visualMediaType + val onMediaPicked = (dialogType as DialogType.GalleryPickerDialog).onPick + GalleryPicker(visualMediaType = visualMediaType) { + onMediaPicked(it) + dialogRequester.dismissDialog() + } + } + else -> { // clear focus from the originating tapped field if (dialogType == null) { From 66a636e54c697ae1c96c38bbb4c4e5d505f3b3c3 Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Thu, 2 May 2024 09:57:51 -0700 Subject: [PATCH 08/32] add proper commits for attachments --- .../screens/map/MapViewModel.kt | 9 ++++++-- .../attachment/AttachmentFormElement.kt | 21 +++++++------------ .../featureforms/internal/utils/Dialog.kt | 15 ++++++------- 3 files changed, 21 insertions(+), 24 deletions(-) diff --git a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/map/MapViewModel.kt b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/map/MapViewModel.kt index 454114de0..66463d636 100644 --- a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/map/MapViewModel.kt +++ b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/map/MapViewModel.kt @@ -194,8 +194,13 @@ class MapViewModel @Inject constructor( IllegalStateException("cannot save feature edit without a ServiceFeatureTable") ) val result = serviceFeatureTable.updateFeature(feature).map { - serviceFeatureTable.serviceGeodatabase?.applyEdits() - ?: throw IllegalStateException("cannot apply feature edit without a ServiceGeodatabase") + serviceFeatureTable.serviceGeodatabase?.let { database -> + return@let if (database.serviceInfo?.canUseServiceGeodatabaseApplyEdits == true) { + database.applyEdits() + } else { + serviceFeatureTable.applyEdits() + } + } feature.refresh() // unselect the feature after the edits have been saved (feature.featureTable?.layer as FeatureLayer).clearSelection() diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt index c1cbde89d..46d0ae27e 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt @@ -18,7 +18,6 @@ package com.arcgismaps.toolkit.featureforms.internal.components.attachment import android.content.Context import android.net.Uri -import android.util.Log import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts @@ -37,6 +36,7 @@ import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Add +import androidx.compose.material.icons.rounded.Photo import androidx.compose.material.icons.rounded.PhotoCamera import androidx.compose.material3.Card import androidx.compose.material3.DropdownMenu @@ -62,8 +62,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp -import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType -import androidx.compose.material.icons.rounded.Photo import com.arcgismaps.LoadStatus import com.arcgismaps.toolkit.featureforms.internal.utils.AttachmentCaptureFileProvider import com.arcgismaps.toolkit.featureforms.internal.utils.DialogType @@ -137,7 +135,6 @@ internal fun AttachmentFormElement( contentType, uri ) - Log.e("TAG", "AttachmentFormElement: $name, $contentType, $uri") onAttachmentAdded(name, contentType, it) } } @@ -193,7 +190,7 @@ private fun Header( @Composable private fun AddAttachment( - hasCameraPermission : Boolean, + hasCameraPermission: Boolean, onAttachment: (String, Uri) -> Unit ) { var showMenu by remember { mutableStateOf(false) } @@ -244,7 +241,7 @@ private fun AddAttachment( }, onClick = { scope.launch { - pickerStyle.emit(PickerStyle.Gallery) + pickerStyle.emit(PickerStyle.PickImage) showMenu = false } } @@ -260,11 +257,9 @@ private fun AddAttachment( }) } - PickerStyle.Gallery -> { + PickerStyle.PickImage -> { dialogRequester.requestDialog( - DialogType.GalleryPickerDialog( - ActivityResultContracts.PickVisualMedia.ImageOnly - ) { uri -> + DialogType.ImagePickerDialog { uri -> onAttachment("image/jpeg", uri) } ) @@ -308,7 +303,7 @@ internal fun ImageCapture(onImageCaptured: (Uri) -> Unit) { } @Composable -internal fun GalleryPicker(visualMediaType: VisualMediaType, onImageSelected: (Uri) -> Unit) { +internal fun ImagePicker(onImageSelected: (Uri) -> Unit) { val launcher = rememberLauncherForActivityResult( contract = ActivityResultContracts.PickVisualMedia() ) { @@ -317,7 +312,7 @@ internal fun GalleryPicker(visualMediaType: VisualMediaType, onImageSelected: (U } } LaunchedEffect(Unit) { - launcher.launch(PickVisualMediaRequest(visualMediaType)) + launcher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) } } @@ -350,7 +345,7 @@ private fun Context.readBytes(uri: Uri): ByteArray? = private sealed class PickerStyle { data object File : PickerStyle() data object Camera : PickerStyle() - data object Gallery : PickerStyle() + data object PickImage : PickerStyle() } @Preview diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt index a6adbe1f8..0da4fd5fd 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt @@ -29,9 +29,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.window.core.layout.WindowSizeClass import androidx.window.layout.WindowMetricsCalculator -import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.VisualMediaType import com.arcgismaps.toolkit.featureforms.R -import com.arcgismaps.toolkit.featureforms.internal.components.attachment.GalleryPicker +import com.arcgismaps.toolkit.featureforms.internal.components.attachment.ImagePicker import com.arcgismaps.toolkit.featureforms.internal.components.attachment.ImageCapture import com.arcgismaps.toolkit.featureforms.internal.components.codedvalue.CodedValueFieldState import com.arcgismaps.toolkit.featureforms.internal.components.codedvalue.ComboBoxDialog @@ -99,9 +98,8 @@ internal sealed class DialogType { data class ImageCaptureDialog(val onImage: (Uri) -> Unit) : DialogType() - data class GalleryPickerDialog( - val visualMediaType: VisualMediaType, - val onPick: (Uri) -> Unit + data class ImagePickerDialog( + val onSelection: (Uri) -> Unit ) : DialogType() } @@ -178,10 +176,9 @@ internal fun FeatureFormDialog() { } } - is DialogType.GalleryPickerDialog -> { - val visualMediaType = (dialogType as DialogType.GalleryPickerDialog).visualMediaType - val onMediaPicked = (dialogType as DialogType.GalleryPickerDialog).onPick - GalleryPicker(visualMediaType = visualMediaType) { + is DialogType.ImagePickerDialog -> { + val onMediaPicked = (dialogType as DialogType.ImagePickerDialog).onSelection + ImagePicker { onMediaPicked(it) dialogRequester.dismissDialog() } From 8f3019b1f62daf9a2135dc9b468f3dbd5bf9589b Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Thu, 2 May 2024 10:15:39 -0700 Subject: [PATCH 09/32] added doc --- .../attachment/AttachmentElementState.kt | 37 +++++++++++++++---- .../attachment/AttachmentFormElement.kt | 18 ++++++--- 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt index 6ea9494ec..150d057e6 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt @@ -51,10 +51,15 @@ import kotlinx.coroutines.launch /** * Represents the state of an [AttachmentFormElement] + * + * @param formElement The form element that this state represents. + * @param scope The coroutine scope used to launch coroutines. + * @param evaluateExpressions A method to evaluates the expressions in the form. */ internal class AttachmentElementState( private val formElement: AttachmentFormElement, - private val scope: CoroutineScope + private val scope: CoroutineScope, + private val evaluateExpressions : suspend () -> Unit ) : FormElementState( label = formElement.label, description = formElement.description, @@ -70,8 +75,6 @@ internal class AttachmentElementState( */ val lazyListState = LazyListState() - val inputType = formElement.input - init { scope.launch { loadAttachments() @@ -97,6 +100,7 @@ internal class AttachmentElementState( */ suspend fun addAttachment(name: String, contentType: String, data: ByteArray) { formElement.addAttachment(name, contentType, data) + evaluateExpressions() // refresh the list of attachments loadAttachments() // load the attachment that was just added @@ -113,7 +117,8 @@ internal class AttachmentElementState( companion object { fun Saver( attachmentFormElement: AttachmentFormElement, - scope: CoroutineScope + scope: CoroutineScope, + evaluateExpressions: suspend () -> Unit ): Saver = listSaver( save = { // save the list of indices of attachments that have been loaded @@ -126,7 +131,7 @@ internal class AttachmentElementState( } }, restore = { savedList -> - AttachmentElementState(attachmentFormElement, scope).also { + AttachmentElementState(attachmentFormElement, scope, evaluateExpressions).also { scope.launch { it.loadAttachments() // load the attachments that were previously loaded @@ -142,6 +147,13 @@ internal class AttachmentElementState( /** * Represents the state of a [FormAttachment]. + * + * @param name The name of the attachment. + * @param size The size of the attachment. + * @param loadStatus The load status of the attachment. + * @param onLoadAttachment A function that loads the attachment. + * @param onLoadThumbnail A function that loads the thumbnail of the attachment. + * @param scope The coroutine scope used to launch coroutines. */ internal class FormAttachmentState( val name: String, @@ -152,8 +164,15 @@ internal class FormAttachmentState( private val scope: CoroutineScope ) { private val _thumbnail: MutableState = mutableStateOf(null) + + /** + * The thumbnail of the attachment. This is `null` until [loadAttachment] is called. + */ val thumbnail: State = _thumbnail + /** + * The type of the attachment. + */ val type: AttachmentType = getAttachmentType(name) constructor(attachment: FormAttachment, scope: CoroutineScope) : this( @@ -165,6 +184,9 @@ internal class FormAttachmentState( scope = scope ) + /** + * Loads the attachment and its thumbnail. + */ fun loadAttachment() { scope.launch { onLoadAttachment().onSuccess { @@ -186,11 +208,12 @@ internal fun rememberAttachmentElementState( val scope = rememberCoroutineScope() return rememberSaveable( inputs = arrayOf(form), - saver = AttachmentElementState.Saver(attachmentFormElement, scope) + saver = AttachmentElementState.Saver(attachmentFormElement, scope, form::evaluateExpressions) ) { AttachmentElementState( formElement = attachmentFormElement, - scope = scope + scope = scope, + evaluateExpressions = form::evaluateExpressions ) } } diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt index 46d0ae27e..f05504e54 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt @@ -132,8 +132,7 @@ internal fun AttachmentFormElement( scope.launch(Dispatchers.IO) { context.readBytes(uri)?.let { val name = attachments.getNewAttachmentNameForContentType( - contentType, - uri + contentType ) onAttachmentAdded(name, contentType, it) } @@ -271,6 +270,10 @@ private fun AddAttachment( } } +/** + * Launches the camera to capture an image. When an image is captured, the [onImageCaptured] callback + * is invoked with the URI of the captured image. + */ @Composable internal fun ImageCapture(onImageCaptured: (Uri) -> Unit) { val context = LocalContext.current @@ -283,7 +286,7 @@ internal fun ImageCapture(onImageCaptured: (Uri) -> Unit) { restore = { Uri.parse(it.first()) } ) ) { - val file = context.createImageFile() + val file = context.createTempImageFile() AttachmentCaptureFileProvider.getImageUri(file, context) } val cameraLauncher = rememberLauncherForActivityResult( @@ -302,6 +305,10 @@ internal fun ImageCapture(onImageCaptured: (Uri) -> Unit) { } } +/** + * Launches the Gallery to select an image. When an image is selected, the [onImageSelected] callback + * is invoked with the URI of the selected image. + */ @Composable internal fun ImagePicker(onImageSelected: (Uri) -> Unit) { val launcher = rememberLauncherForActivityResult( @@ -317,8 +324,7 @@ internal fun ImagePicker(onImageSelected: (Uri) -> Unit) { } private fun List.getNewAttachmentNameForContentType( - contentType: String, - uri: Uri + contentType: String ): String { val (attachmentType: AttachmentType, ext: String) = when (contentType) { "image/jpeg" -> Pair(AttachmentType.Image, "jpg") @@ -328,7 +334,7 @@ private fun List.getNewAttachmentNameForContentType( return "$attachmentType $count.$ext" } -private fun Context.createImageFile(): File { +private fun Context.createTempImageFile(): File { val timeStamp = Instant.now().toEpochMilli() val dir = File(cacheDir, "feature_forms_attachments") dir.mkdirs() From 089357137573ae3e3bf5224f33a20afa7ea9131f Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Thu, 2 May 2024 11:46:41 -0700 Subject: [PATCH 10/32] add editability of attachmentformelement --- .../screens/map/MapViewModel.kt | 25 ++++++++++++++----- .../attachment/AttachmentElementState.kt | 2 ++ .../attachment/AttachmentFormElement.kt | 4 ++- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/map/MapViewModel.kt b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/map/MapViewModel.kt index 66463d636..a69c3370e 100644 --- a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/map/MapViewModel.kt +++ b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/map/MapViewModel.kt @@ -36,6 +36,8 @@ import com.arcgismaps.mapping.featureforms.FieldFormElement import com.arcgismaps.mapping.featureforms.FormElement import com.arcgismaps.mapping.featureforms.GroupFormElement import com.arcgismaps.mapping.layers.FeatureLayer +import com.arcgismaps.mapping.layers.GroupLayer +import com.arcgismaps.mapping.layers.Layer import com.arcgismaps.mapping.view.SingleTapConfirmedEvent import com.arcgismaps.toolkit.featureforms.ValidationErrorVisibility import com.arcgismaps.toolkit.featureformsapp.data.PortalItemRepository @@ -139,12 +141,7 @@ class MapViewModel @Inject constructor( private suspend fun checkFeatureFormDefinition() { map.load() val layer = map.operationalLayers.firstOrNull { - if (it is FeatureLayer) { - it.load() - it.featureFormDefinition != null - } else { - false - } + it.hasFeatureFormDefinition() } _uiState.value = if (layer == null) { UIState.NoFeatureFormDefinition @@ -337,3 +334,19 @@ fun List.getFormElement(fieldName: String): FieldFormElement? { } } } + +/** + * Returns true if the layer has a feature form definition. If the layer is a [GroupLayer] then + * this function will return true if any of the layers in the group have a feature form definition. + */ +private suspend fun Layer.hasFeatureFormDefinition(): Boolean = when(this) { + is FeatureLayer -> { + load() + featureFormDefinition != null + } + is GroupLayer -> { + load() + layers.any { it.hasFeatureFormDefinition() } + } + else -> false +} diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt index 150d057e6..50862b4d4 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt @@ -70,6 +70,8 @@ internal class AttachmentElementState( */ val attachments = SnapshotStateList() + val isEditable = formElement.isEditable + /** * The state of the lazy list that displays the [attachments]. */ diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt index f05504e54..35fc56471 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt @@ -47,6 +47,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -80,10 +81,11 @@ internal fun AttachmentFormElement( ) { val scope = rememberCoroutineScope() val context = LocalContext.current + val editable by state.isEditable.collectAsState() AttachmentFormElement( label = state.label, description = state.description, - editable = true, + editable = editable, attachments = state.attachments, lazyListState = state.lazyListState, hasCameraPermission = state.hasCameraPermissions(context), From 950923ceb187a2967696315fec66367b8c82a997 Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Fri, 3 May 2024 18:13:29 -0700 Subject: [PATCH 11/32] update doc --- .../internal/components/attachment/AttachmentElementState.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt index 50862b4d4..713e6bd4f 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt @@ -70,6 +70,9 @@ internal class AttachmentElementState( */ val attachments = SnapshotStateList() + /** + * Indicates whether the attachment form element is editable. + */ val isEditable = formElement.isEditable /** From f4d169cd76b9ecb9db97036dfe5cf0769bf0c6e7 Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Fri, 3 May 2024 19:00:10 -0700 Subject: [PATCH 12/32] extract string resource --- .../internal/components/attachment/AttachmentFormElement.kt | 6 ++++-- toolkit/featureforms/src/main/res/values/strings.xml | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt index 35fc56471..ad1431324 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt @@ -59,11 +59,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import com.arcgismaps.LoadStatus +import com.arcgismaps.toolkit.featureforms.R import com.arcgismaps.toolkit.featureforms.internal.utils.AttachmentCaptureFileProvider import com.arcgismaps.toolkit.featureforms.internal.utils.DialogType import com.arcgismaps.toolkit.featureforms.internal.utils.LocalDialogRequester @@ -215,7 +217,7 @@ private fun AddAttachment( ) { if (hasCameraPermission) { DropdownMenuItem( - text = { Text(text = "Take Photo") }, + text = { Text(text = stringResource(R.string.take_photo)) }, trailingIcon = { Icon( imageVector = Icons.Rounded.PhotoCamera, @@ -232,7 +234,7 @@ private fun AddAttachment( ) } DropdownMenuItem( - text = { Text(text = "Add Photo") }, + text = { Text(text = stringResource(R.string.add_photo)) }, trailingIcon = { Icon( imageVector = Icons.Rounded.Photo, diff --git a/toolkit/featureforms/src/main/res/values/strings.xml b/toolkit/featureforms/src/main/res/values/strings.xml index 18e08a74d..c21c8825b 100644 --- a/toolkit/featureforms/src/main/res/values/strings.xml +++ b/toolkit/featureforms/src/main/res/values/strings.xml @@ -89,4 +89,6 @@ for minutes Select AM or PM PM + Take Photo + Add Photo From 70bb4efa951426ee715e92cf868fe9974ec83cee Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Mon, 6 May 2024 10:22:55 -0700 Subject: [PATCH 13/32] add delete and rename attachments --- .../attachment/AttachmentElementState.kt | 54 +++- .../attachment/AttachmentFormElement.kt | 4 +- .../components/attachment/AttachmentTile.kt | 272 ++++++++++++++---- .../featureforms/internal/utils/Dialog.kt | 22 +- .../src/main/res/values/strings.xml | 4 + 5 files changed, 297 insertions(+), 59 deletions(-) diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt index 713e6bd4f..8fcd5a7d4 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt @@ -48,6 +48,7 @@ import com.arcgismaps.toolkit.featureforms.internal.components.base.FormElementS import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch +import java.util.Objects /** * Represents the state of an [AttachmentFormElement] @@ -59,7 +60,7 @@ import kotlinx.coroutines.launch internal class AttachmentElementState( private val formElement: AttachmentFormElement, private val scope: CoroutineScope, - private val evaluateExpressions : suspend () -> Unit + private val evaluateExpressions: suspend () -> Unit ) : FormElementState( label = formElement.label, description = formElement.description, @@ -95,7 +96,7 @@ internal class AttachmentElementState( attachments.clear() attachments.addAll( formElement.attachments.map { - FormAttachmentState(it, scope) + FormAttachmentState(this, it, scope) } ) } @@ -114,6 +115,18 @@ internal class AttachmentElementState( lazyListState.scrollToItem(attachments.size - 1) } + suspend fun deleteAttachment(formAttachment: FormAttachment) { + formElement.deleteAttachment(formAttachment) + loadAttachments() + } + + suspend fun renameAttachment(formAttachment: FormAttachment, newName: String) { + if (formAttachment.name != newName) { + formElement.renameAttachment(formAttachment, newName) + loadAttachments() + } + } + fun hasCameraPermissions(context: Context): Boolean = ContextCompat.checkSelfPermission( context, Manifest.permission.CAMERA @@ -166,6 +179,8 @@ internal class FormAttachmentState( val loadStatus: StateFlow, private val onLoadAttachment: suspend () -> Result, private val onLoadThumbnail: suspend () -> Result, + val deleteAttachment: suspend () -> Unit, + val renameAttachment: suspend (String) -> Unit, private val scope: CoroutineScope ) { private val _thumbnail: MutableState = mutableStateOf(null) @@ -180,12 +195,22 @@ internal class FormAttachmentState( */ val type: AttachmentType = getAttachmentType(name) - constructor(attachment: FormAttachment, scope: CoroutineScope) : this( + constructor( + element: AttachmentElementState, + attachment: FormAttachment, + scope: CoroutineScope + ) : this( name = attachment.name, size = attachment.size, loadStatus = attachment.loadStatus, onLoadAttachment = attachment::load, onLoadThumbnail = attachment::createFullImage, + deleteAttachment = { + element.deleteAttachment(attachment) + }, + renameAttachment = { + element.renameAttachment(attachment, it) + }, scope = scope ) @@ -203,6 +228,23 @@ internal class FormAttachmentState( } } } + + override fun hashCode(): Int { + return Objects.hash(name, size, type) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as FormAttachmentState + + if (name != other.name) return false + if (size != other.size) return false + if (type != other.type) return false + + return true + } } @Composable @@ -213,7 +255,11 @@ internal fun rememberAttachmentElementState( val scope = rememberCoroutineScope() return rememberSaveable( inputs = arrayOf(form), - saver = AttachmentElementState.Saver(attachmentFormElement, scope, form::evaluateExpressions) + saver = AttachmentElementState.Saver( + attachmentFormElement, + scope, + form::evaluateExpressions + ) ) { AttachmentElementState( formElement = attachmentFormElement, diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt index ad1431324..0dbe5ba5f 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt @@ -156,7 +156,7 @@ private fun Carousel(state: LazyListState, attachments: List LoadedView( - thumbnail = thumbnail, - title = state.name - ) + Box(modifier = Modifier) { + when (loadStatus) { + LoadStatus.Loaded -> LoadedView( + thumbnail = thumbnail, + title = state.name + ) - LoadStatus.Loading -> DefaultView( - title = state.name, - size = state.size, - isLoading = true, - isError = false - ) + LoadStatus.Loading -> DefaultView( + title = state.name, + size = state.size, + isLoading = true, + isError = false + ) - LoadStatus.NotLoaded -> DefaultView( - title = state.name, - size = state.size, - isLoading = false, - isError = false - ) + LoadStatus.NotLoaded -> DefaultView( + title = state.name, + size = state.size, + isLoading = false, + isError = false + ) - is LoadStatus.FailedToLoad -> DefaultView( - title = state.name, - size = state.size, - isLoading = false, - isError = true - ) + is LoadStatus.FailedToLoad -> DefaultView( + title = state.name, + size = state.size, + isLoading = false, + isError = true + ) + } + DropdownMenu( + expanded = showContextMenu, + onDismissRequest = { showContextMenu = false } + ) { + DropdownMenuItem( + text = { Text(text = stringResource(R.string.rename)) }, + leadingIcon = { + Icon( + imageVector = Icons.Outlined.EditNote, + contentDescription = null + ) + }, + onClick = { + showContextMenu = false + dialogRequester.requestDialog(DialogType.RenameAttachmentDialog( + name = state.name, + ) { + scope.launch { + state.renameAttachment(it) + } + }) + }) + DropdownMenuItem( + text = { Text(text = stringResource(R.string.delete)) }, + leadingIcon = { + Icon( + imageVector = Icons.Outlined.Delete, + contentDescription = null + ) + + }, + colors = MenuDefaults.itemColors( + textColor = MaterialTheme.colorScheme.error, + leadingIconColor = MaterialTheme.colorScheme.error + ), + onClick = { + showContextMenu = false + scope.launch { + state.deleteAttachment() + } + }) + } + } + } + LaunchedEffect(interactionSource) { + var wasALongPress = false + interactionSource.interactions.collectLatest { + when (it) { + is PressInteraction.Press -> { + wasALongPress = false + delay(configuration.longPressTimeoutMillis) + wasALongPress = true + // handle long press + if (loadStatus is LoadStatus.Loaded) { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + showContextMenu = true + } + } + + is PressInteraction.Release -> { + if (!wasALongPress) { + haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) + // handle single tap + if (loadStatus is LoadStatus.NotLoaded || loadStatus is LoadStatus.FailedToLoad) { + // load attachment + state.loadAttachment() + } else if (loadStatus is LoadStatus.Loaded) { + // open attachment + } + } + } + } } } } @@ -157,8 +245,8 @@ private fun LoadedView( imageVector = attachmentType.getIcon(), contentDescription = null, modifier = Modifier - .fillMaxSize() .padding(top = 10.dp, bottom = 25.dp) + .fillMaxSize(0.8f) .align(Alignment.Center) ) } @@ -273,3 +361,81 @@ private fun Size( .padding(horizontal = 1.dp) ) } + +@Composable +internal fun RenameAttachmentDialog( + name: String, + onRename: (String) -> Unit, + onDismissRequest: () -> Unit +) { + val groups = remember(name) { name.split("\\.(?=[^\\\\.]+\$)".toRegex()) } + var filename by remember(groups) { mutableStateOf(groups.first()) } + val extension = remember(groups) { if (groups.count() == 2) groups.last() else "" } + val focusRequester = remember { FocusRequester() } + Dialog( + onDismissRequest = onDismissRequest, + properties = DialogProperties(usePlatformDefaultWidth = true), + content = { + Surface( + modifier = Modifier.wrapContentSize(), + shape = RoundedCornerShape(5.dp) + ) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = stringResource(R.string.rename_attachment), + fontSize = 18.sp, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.height(25.dp)) + TextField( + value = TextFieldValue( + text = filename, + selection = TextRange(filename.length) + ), + onValueChange = { value -> filename = value.text }, + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + label = { Text(stringResource(R.string.name)) }, + suffix = { + Text(text = ".$extension") + } + ) + Spacer(modifier = Modifier.height(25.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically + ) { + Button(onClick = onDismissRequest) { + Text(text = stringResource(id = R.string.cancel)) + } + Button( + onClick = { onRename("$filename.$extension") }, + enabled = filename.isNotEmpty() + ) { + Text(text = stringResource(id = R.string.rename)) + } + } + } + } + } + ) + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } +} + +@Preview +@Composable +private fun RenameAttachmentDialogPreview() { + RenameAttachmentDialog( + name = "Photo 1.jpg", + onRename = {}, + onDismissRequest = {} + ) +} diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt index 0da4fd5fd..a6bda939b 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt @@ -30,8 +30,9 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.window.core.layout.WindowSizeClass import androidx.window.layout.WindowMetricsCalculator import com.arcgismaps.toolkit.featureforms.R -import com.arcgismaps.toolkit.featureforms.internal.components.attachment.ImagePicker import com.arcgismaps.toolkit.featureforms.internal.components.attachment.ImageCapture +import com.arcgismaps.toolkit.featureforms.internal.components.attachment.ImagePicker +import com.arcgismaps.toolkit.featureforms.internal.components.attachment.RenameAttachmentDialog import com.arcgismaps.toolkit.featureforms.internal.components.codedvalue.CodedValueFieldState import com.arcgismaps.toolkit.featureforms.internal.components.codedvalue.ComboBoxDialog import com.arcgismaps.toolkit.featureforms.internal.components.datetime.DateTimeFieldState @@ -101,6 +102,11 @@ internal sealed class DialogType { data class ImagePickerDialog( val onSelection: (Uri) -> Unit ) : DialogType() + + data class RenameAttachmentDialog( + val name : String, + val onRename: (String) -> Unit + ) : DialogType() } /** @@ -184,6 +190,20 @@ internal fun FeatureFormDialog() { } } + is DialogType.RenameAttachmentDialog -> { + val onRenameAttachment = (dialogType as DialogType.RenameAttachmentDialog).onRename + val name = (dialogType as DialogType.RenameAttachmentDialog).name + RenameAttachmentDialog( + name = name, + onRename = { + onRenameAttachment(it) + dialogRequester.dismissDialog() + } + ) { + dialogRequester.dismissDialog() + } + } + else -> { // clear focus from the originating tapped field if (dialogType == null) { diff --git a/toolkit/featureforms/src/main/res/values/strings.xml b/toolkit/featureforms/src/main/res/values/strings.xml index c21c8825b..19afcdb0e 100644 --- a/toolkit/featureforms/src/main/res/values/strings.xml +++ b/toolkit/featureforms/src/main/res/values/strings.xml @@ -91,4 +91,8 @@ PM Take Photo Add Photo + Rename + Delete + Rename Attachment + Name From a13e6ed72e2653f01abc5d21235c741116a245f1 Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Tue, 7 May 2024 09:46:03 -0700 Subject: [PATCH 14/32] initial fix --- .../toolkit/featureforms/FeatureForm.kt | 3 +- .../attachment/AttachmentElementState.kt | 7 ++- .../attachment/AttachmentFormElement.kt | 33 +++++------- .../components/base/BaseFieldState.kt | 5 +- .../components/base/BaseGroupState.kt | 10 ++-- .../components/base/FormElementState.kt | 4 +- .../components/base/FormStateCollection.kt | 15 +++++- .../codedvalue/CodedValueFieldState.kt | 3 ++ .../components/codedvalue/ComboBoxField.kt | 3 +- .../codedvalue/ComboBoxFieldState.kt | 5 +- .../codedvalue/RadioButtonFieldState.kt | 4 ++ .../components/codedvalue/SwitchFieldState.kt | 4 ++ .../components/datetime/DateTimeField.kt | 3 +- .../components/datetime/DateTimeFieldState.kt | 9 ++++ .../components/text/FormTextFieldState.kt | 4 ++ .../featureforms/internal/utils/Dialog.kt | 50 +++++++++++++++---- 16 files changed, 120 insertions(+), 42 deletions(-) diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/FeatureForm.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/FeatureForm.kt index 3508f312c..a54d7dc49 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/FeatureForm.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/FeatureForm.kt @@ -18,6 +18,7 @@ package com.arcgismaps.toolkit.featureforms +import android.util.Log import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -180,7 +181,7 @@ private fun FeatureForm( // expressions evaluated, load attachments formElements.value = featureForm.elements } - FeatureFormDialog() + FeatureFormDialog(states) // launch a new side effect in a launched effect when validationErrorVisibility changes LaunchedEffect(validationErrorVisibility) { // if it set to always show errors force each field to validate itself and show any errors diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt index 713e6bd4f..e510059d9 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt @@ -20,6 +20,7 @@ import android.Manifest import android.content.Context import android.content.pm.PackageManager import android.graphics.drawable.BitmapDrawable +import android.util.Log import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.AudioFile @@ -59,11 +60,13 @@ import kotlinx.coroutines.launch internal class AttachmentElementState( private val formElement: AttachmentFormElement, private val scope: CoroutineScope, + id : Int, private val evaluateExpressions : suspend () -> Unit ) : FormElementState( label = formElement.label, description = formElement.description, isVisible = formElement.isVisible, + id = id ) { /** * The attachments associated with the form element. @@ -84,6 +87,7 @@ internal class AttachmentElementState( scope.launch { loadAttachments() } + Log.e("TAG", "id: $id, ${hashCode()}", ) } /** @@ -136,7 +140,7 @@ internal class AttachmentElementState( } }, restore = { savedList -> - AttachmentElementState(attachmentFormElement, scope, evaluateExpressions).also { + AttachmentElementState(attachmentFormElement, scope, attachmentFormElement.hashCode() , evaluateExpressions).also { scope.launch { it.loadAttachments() // load the attachments that were previously loaded @@ -218,6 +222,7 @@ internal fun rememberAttachmentElementState( AttachmentElementState( formElement = attachmentFormElement, scope = scope, + id = attachmentFormElement.hashCode(), evaluateExpressions = form::evaluateExpressions ) } diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt index ad1431324..ca4dc0c0c 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt @@ -88,6 +88,7 @@ internal fun AttachmentFormElement( label = state.label, description = state.description, editable = editable, + stateId = state.id, attachments = state.attachments, lazyListState = state.lazyListState, hasCameraPermission = state.hasCameraPermissions(context), @@ -105,6 +106,7 @@ internal fun AttachmentFormElement( label: String, description: String, editable: Boolean, + stateId : Int, attachments: List, lazyListState: LazyListState, hasCameraPermission: Boolean, @@ -131,17 +133,9 @@ internal fun AttachmentFormElement( if (editable) { // Add attachment button AddAttachment( + stateId = stateId, hasCameraPermission = hasCameraPermission - ) { contentType, uri -> - scope.launch(Dispatchers.IO) { - context.readBytes(uri)?.let { - val name = attachments.getNewAttachmentNameForContentType( - contentType - ) - onAttachmentAdded(name, contentType, it) - } - } - } + ) } } Spacer(modifier = Modifier.height(20.dp)) @@ -193,8 +187,8 @@ private fun Header( @Composable private fun AddAttachment( + stateId : Int, hasCameraPermission: Boolean, - onAttachment: (String, Uri) -> Unit ) { var showMenu by remember { mutableStateOf(false) } val dialogRequester = LocalDialogRequester.current @@ -255,15 +249,16 @@ private fun AddAttachment( pickerStyle.collect { when (it) { PickerStyle.Camera -> { - dialogRequester.requestDialog(DialogType.ImageCaptureDialog { uri -> - onAttachment("image/jpeg", uri) - }) + dialogRequester.requestDialog(DialogType.ImageCaptureDialog( + stateId = stateId, + contentType = "image/jpeg" + )) } PickerStyle.PickImage -> { dialogRequester.requestDialog( DialogType.ImagePickerDialog { uri -> - onAttachment("image/jpeg", uri) + //onAttachment("image/jpeg", uri) } ) } @@ -327,7 +322,7 @@ internal fun ImagePicker(onImageSelected: (Uri) -> Unit) { } } -private fun List.getNewAttachmentNameForContentType( +internal fun List.getNewAttachmentNameForContentType( contentType: String ): String { val (attachmentType: AttachmentType, ext: String) = when (contentType) { @@ -338,7 +333,7 @@ private fun List.getNewAttachmentNameForContentType( return "$attachmentType $count.$ext" } -private fun Context.createTempImageFile(): File { +internal fun Context.createTempImageFile(): File { val timeStamp = Instant.now().toEpochMilli() val dir = File(cacheDir, "feature_forms_attachments") dir.mkdirs() @@ -349,9 +344,6 @@ private fun Context.createTempImageFile(): File { ) } -private fun Context.readBytes(uri: Uri): ByteArray? = - contentResolver.openInputStream(uri)?.use { it.buffered().readBytes() } - private sealed class PickerStyle { data object File : PickerStyle() data object Camera : PickerStyle() @@ -365,6 +357,7 @@ private fun AttachmentFormElementPreview() { label = "Attachments", description = "Add attachments", editable = true, + stateId = 1, attachments = listOf( FormAttachmentState( "Photo 1.jpg", diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/base/BaseFieldState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/base/BaseFieldState.kt index b20a60dd4..cf2fb61d9 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/base/BaseFieldState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/base/BaseFieldState.kt @@ -55,6 +55,7 @@ internal data class Value( * Base state class for any Field within a feature form. It provides the default set of properties * that are common to all [FieldFormElement]'s. * + * @param id Unique identifier for the field. * @param properties the [FieldProperties] associated with this state. * @param initialValue optional initial value to set for this field. It is set to the value of * [FieldProperties.value] by default. @@ -65,15 +66,17 @@ internal data class Value( * called after a successful [updateValue]. */ internal abstract class BaseFieldState( + id: Int, properties: FieldProperties, initialValue: T = properties.value.value, private val scope: CoroutineScope, private val updateValue: (Any?) -> Unit, private val evaluateExpressions: suspend () -> Result>, ) : FormElementState( + id = id, label = properties.label, description = properties.description, - isVisible = properties.visible + isVisible = properties.visible, ) { /** * Placeholder hint for the field. diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/base/BaseGroupState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/base/BaseGroupState.kt index fb3b5234b..c3fce82b1 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/base/BaseGroupState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/base/BaseGroupState.kt @@ -27,15 +27,17 @@ import com.arcgismaps.mapping.featureforms.GroupFormElement import kotlinx.coroutines.flow.StateFlow internal class BaseGroupState( + id : Int, label: String, description: String, isVisible: StateFlow, expanded: Boolean, val fieldStates: FormStateCollection ) : FormElementState( + id = id, label = label, description = description, - isVisible = isVisible + isVisible = isVisible, ) { private val _expanded = mutableStateOf(expanded) val expanded: State = _expanded @@ -54,11 +56,12 @@ internal class BaseGroupState( }, restore = { BaseGroupState( + id = groupElement.hashCode(), label = groupElement.label, description = groupElement.description, isVisible = groupElement.isVisible, expanded = it, - fieldStates = fieldStates + fieldStates = fieldStates, ) } ) @@ -75,10 +78,11 @@ internal fun rememberBaseGroupState( saver = BaseGroupState.Saver(groupElement, fieldStates) ) { BaseGroupState( + id = groupElement.hashCode(), label = groupElement.label, description = groupElement.description, isVisible = groupElement.isVisible, expanded = groupElement.initialState == FormGroupState.Expanded, - fieldStates = fieldStates + fieldStates = fieldStates, ) } diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/base/FormElementState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/base/FormElementState.kt index 3d45e921c..a0e2d4a5e 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/base/FormElementState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/base/FormElementState.kt @@ -23,13 +23,15 @@ import kotlinx.coroutines.flow.StateFlow /** * Base state class for a [FormElement]. * + * @param id Unique identifier for the field. * @param label Title for the field. * @param description Description text for the field. * @param isVisible Property that indicates if the field is visible. */ @Immutable internal abstract class FormElementState( + val id : Int, val label : String, val description: String, - val isVisible : StateFlow + val isVisible : StateFlow, ) diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/base/FormStateCollection.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/base/FormStateCollection.kt index afa8f7216..4791157c5 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/base/FormStateCollection.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/base/FormStateCollection.kt @@ -33,6 +33,15 @@ internal interface FormStateCollection : Iterable { * @return the [FormElementState] associated with the formElement, or null if none. */ operator fun get(formElement: FormElement): FormElementState? + + /** + * Provides the bracket operator to the collection. + * + * @param id the unique identifier [FormElementState.id] + * @return the [FormElementState] associated with the id, or null if none. + */ + operator fun get(id: Int): FormElementState? + interface Entry { val formElement: FormElement val state: FormElementState @@ -64,7 +73,8 @@ internal interface MutableFormStateCollection : FormStateCollection { /** * Creates a new [MutableFormStateCollection]. */ -internal fun MutableFormStateCollection(): MutableFormStateCollection = MutableFormStateCollectionImpl() +internal fun MutableFormStateCollection(): MutableFormStateCollection = + MutableFormStateCollectionImpl() /** * Default implementation for a [MutableFormStateCollection]. @@ -84,6 +94,9 @@ private class MutableFormStateCollectionImpl : MutableFormStateCollection { override operator fun get(formElement: FormElement): FormElementState? = entries.firstOrNull { it.formElement == formElement }?.state + override fun get(id: Int): FormElementState? = + entries.firstOrNull { it.formElement.hashCode() == id }?.state + /** * Default implementation for a [FormStateCollection.Entry]. */ diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/codedvalue/CodedValueFieldState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/codedvalue/CodedValueFieldState.kt index 58afc5d06..886029b8a 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/codedvalue/CodedValueFieldState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/codedvalue/CodedValueFieldState.kt @@ -47,6 +47,7 @@ internal open class CodedValueFieldProperties( * A class to handle the state of any coded value type. Essential properties are inherited * from the [BaseFieldState]. * + * @param id Unique identifier for the field. * @param properties the [CodedValueFieldProperties] associated with this state. * @param initialValue optional initial value to set for this field. It is set to the value of * [TextFieldProperties.value] by default. @@ -57,12 +58,14 @@ internal open class CodedValueFieldProperties( * called after a successful [updateValue]. */ internal abstract class CodedValueFieldState( + id : Int, properties: CodedValueFieldProperties, initialValue: Any? = properties.value.value, scope: CoroutineScope, updateValue: (Any?) -> Unit, evaluateExpressions: suspend () -> Result> ) : BaseFieldState( + id = id, properties = properties, scope = scope, initialValue = initialValue, diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/codedvalue/ComboBoxField.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/codedvalue/ComboBoxField.kt index ed8658b25..12b6c2096 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/codedvalue/ComboBoxField.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/codedvalue/ComboBoxField.kt @@ -141,7 +141,7 @@ internal fun ComboBoxField( interactionSource.interactions.collect { if (it is PressInteraction.Release) { if (isEditable) { - dialogRequester.requestDialog(DialogType.ComboBoxDialog(state)) + dialogRequester.requestDialog(DialogType.ComboBoxDialog(state.id)) } } } @@ -366,6 +366,7 @@ private fun ComboBoxPreview() { noValueLabel = "No value" ), scope = scope, + id = 1, updateValue = {}, evaluateExpressions = { return@ComboBoxFieldState Result.success(emptyList()) diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/codedvalue/ComboBoxFieldState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/codedvalue/ComboBoxFieldState.kt index 2dd81ba23..064ada651 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/codedvalue/ComboBoxFieldState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/codedvalue/ComboBoxFieldState.kt @@ -32,12 +32,13 @@ import kotlinx.coroutines.launch * A concrete class for use with a [ComboBoxField]. */ internal class ComboBoxFieldState( + id : Int, properties: CodedValueFieldProperties, initialValue: Any? = properties.value.value, scope: CoroutineScope, updateValue: (Any?) -> Unit, evaluateExpressions: suspend () -> Result> -) : CodedValueFieldState(properties, initialValue, scope, updateValue, evaluateExpressions) { +) : CodedValueFieldState(id, properties, initialValue, scope, updateValue, evaluateExpressions) { companion object { /** @@ -58,6 +59,7 @@ internal class ComboBoxFieldState( restore = { list -> val input = formElement.input as ComboBoxFormInput ComboBoxFieldState( + id = formElement.hashCode(), properties = CodedValueFieldProperties( label = formElement.label, placeholder = formElement.hint, @@ -95,6 +97,7 @@ internal fun rememberComboBoxFieldState( ) { val input = field.input as ComboBoxFormInput ComboBoxFieldState( + id = field.hashCode(), properties = CodedValueFieldProperties( label = field.label, placeholder = field.hint, diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/codedvalue/RadioButtonFieldState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/codedvalue/RadioButtonFieldState.kt index 9e53e3a41..58475c4b2 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/codedvalue/RadioButtonFieldState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/codedvalue/RadioButtonFieldState.kt @@ -30,12 +30,14 @@ import kotlinx.coroutines.CoroutineScope internal typealias RadioButtonFieldProperties = CodedValueFieldProperties internal class RadioButtonFieldState( + id : Int, properties: RadioButtonFieldProperties, initialValue: Any? = properties.value.value, scope: CoroutineScope, updateValue: (Any?) -> Unit, evaluateExpressions: suspend () -> Result> ) : CodedValueFieldState( + id, properties = properties, initialValue = initialValue, scope = scope, @@ -73,6 +75,7 @@ internal class RadioButtonFieldState( restore = { list -> val input = formElement.input as RadioButtonsFormInput RadioButtonFieldState( + id = formElement.hashCode(), properties = RadioButtonFieldProperties( label = formElement.label, placeholder = formElement.hint, @@ -108,6 +111,7 @@ internal fun rememberRadioButtonFieldState( ) { val input = field.input as RadioButtonsFormInput RadioButtonFieldState( + id = field.hashCode(), properties = RadioButtonFieldProperties( label = field.label, placeholder = field.hint, diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/codedvalue/SwitchFieldState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/codedvalue/SwitchFieldState.kt index 0fddcbf64..0c7ff94f0 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/codedvalue/SwitchFieldState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/codedvalue/SwitchFieldState.kt @@ -80,12 +80,14 @@ internal class SwitchFieldProperties( */ @Stable internal class SwitchFieldState( + id : Int, properties: SwitchFieldProperties, val initialValue: Any? = properties.value.value, scope: CoroutineScope, updateValue: (Any?) -> Unit, evaluateExpressions: suspend () -> Result> ) : CodedValueFieldState( + id = id, properties = properties, scope = scope, initialValue = initialValue, @@ -123,6 +125,7 @@ internal class SwitchFieldState( restore = { list -> val input = formElement.input as SwitchFormInput SwitchFieldState( + id = formElement.hashCode(), properties = SwitchFieldProperties( label = formElement.label, placeholder = formElement.hint, @@ -167,6 +170,7 @@ internal fun rememberSwitchFieldState( val fallback = initialValue.isEmpty() || (field.value.value != input.onValue.code && field.value.value != input.offValue.code) SwitchFieldState( + id = field.hashCode(), properties = SwitchFieldProperties( label = field.label, placeholder = field.hint, diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/datetime/DateTimeField.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/datetime/DateTimeField.kt index 65e3c2496..19d6d17ec 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/datetime/DateTimeField.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/datetime/DateTimeField.kt @@ -97,7 +97,7 @@ internal fun DateTimeField( // request to show the date picker dialog only when the touch is released // the dialog is responsible for updating the value on the state if (isEditable) { - dialogRequester.requestDialog(DialogType.DateTimeDialog(state)) + dialogRequester.requestDialog(DialogType.DateTimeDialog(state.id)) } } } @@ -125,6 +125,7 @@ private fun DateTimeFieldPreview() { ), scope = scope, updateValue = {}, + id = 1, evaluateExpressions = { return@DateTimeFieldState Result.success(emptyList()) } diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/datetime/DateTimeFieldState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/datetime/DateTimeFieldState.kt index 60fb0e6a5..ce0bd5b63 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/datetime/DateTimeFieldState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/datetime/DateTimeFieldState.kt @@ -18,6 +18,7 @@ package com.arcgismaps.toolkit.featureforms.internal.components.datetime +import android.util.Log import androidx.compose.runtime.Composable import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.listSaver @@ -66,12 +67,14 @@ internal class DateTimeFieldProperties( * called after a successful [updateValue]. */ internal class DateTimeFieldState( + id : Int, properties: DateTimeFieldProperties, initialValue: Instant? = properties.value.value, scope: CoroutineScope, updateValue: (Any?) -> Unit, evaluateExpressions: suspend () -> Result> ) : BaseFieldState( + id = id, properties = properties, initialValue = initialValue, scope = scope, @@ -85,6 +88,10 @@ internal class DateTimeFieldState( val shouldShowTime: Boolean = properties.shouldShowTime override fun typeConverter(input: Instant?): Any? = input + + init { + Log.e("TAG", "id : $label: ${hashCode()}", ) + } companion object { fun Saver( @@ -98,6 +105,7 @@ internal class DateTimeFieldState( restore = { list -> val input = field.input as DateTimePickerFormInput DateTimeFieldState( + id = field.hashCode(), properties = DateTimeFieldProperties( label = field.label, placeholder = field.hint, @@ -140,6 +148,7 @@ internal fun rememberDateTimeFieldState( ) ) { DateTimeFieldState( + id = field.hashCode(), properties = DateTimeFieldProperties( label = field.label, placeholder = field.hint, diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/text/FormTextFieldState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/text/FormTextFieldState.kt index bac2577d6..7fabd8146 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/text/FormTextFieldState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/text/FormTextFieldState.kt @@ -68,12 +68,14 @@ internal class TextFieldProperties( */ @Stable internal class FormTextFieldState( + id : Int, properties: TextFieldProperties, initialValue: String = properties.value.value, scope: CoroutineScope, updateValue: (Any?) -> Unit, evaluateExpressions: suspend () -> Result> ) : BaseFieldState( + id = id, properties = properties, initialValue = initialValue, scope = scope, @@ -132,6 +134,7 @@ internal class FormTextFieldState( val maxLength = (formElement.input as? TextBoxFormInput)?.maxLength ?: (formElement.input as TextAreaFormInput).maxLength FormTextFieldState( + id = formElement.hashCode(), properties = TextFieldProperties( label = formElement.label, placeholder = formElement.hint, @@ -172,6 +175,7 @@ internal fun rememberFormTextFieldState( saver = FormTextFieldState.Saver(field, form, scope) ) { FormTextFieldState( + id = field.hashCode(), properties = TextFieldProperties( label = field.label, placeholder = field.hint, diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt index 0da4fd5fd..19a641868 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt @@ -18,20 +18,26 @@ package com.arcgismaps.toolkit.featureforms.internal.utils import android.content.Context import android.net.Uri +import android.util.Log import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.window.core.layout.WindowSizeClass import androidx.window.layout.WindowMetricsCalculator import com.arcgismaps.toolkit.featureforms.R -import com.arcgismaps.toolkit.featureforms.internal.components.attachment.ImagePicker +import com.arcgismaps.toolkit.featureforms.internal.components.attachment.AttachmentElementState import com.arcgismaps.toolkit.featureforms.internal.components.attachment.ImageCapture +import com.arcgismaps.toolkit.featureforms.internal.components.attachment.ImagePicker +import com.arcgismaps.toolkit.featureforms.internal.components.attachment.getNewAttachmentNameForContentType +import com.arcgismaps.toolkit.featureforms.internal.components.base.FormStateCollection import com.arcgismaps.toolkit.featureforms.internal.components.codedvalue.CodedValueFieldState import com.arcgismaps.toolkit.featureforms.internal.components.codedvalue.ComboBoxDialog import com.arcgismaps.toolkit.featureforms.internal.components.datetime.DateTimeFieldState @@ -42,6 +48,7 @@ import com.arcgismaps.toolkit.featureforms.internal.components.datetime.picker.r import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch import java.time.Instant /** @@ -87,16 +94,19 @@ internal sealed class DialogType { * * @param state The [CodedValueFieldState] to use for the dialog. */ - data class ComboBoxDialog(val state: CodedValueFieldState) : DialogType() + data class ComboBoxDialog(val stateId: Int) : DialogType() /** * Indicates a [DateTimePicker]. * * @param state The [DateTimeFieldState] to use for the dialog. */ - data class DateTimeDialog(val state: DateTimeFieldState) : DialogType() + data class DateTimeDialog(val stateId : Int) : DialogType() - data class ImageCaptureDialog(val onImage: (Uri) -> Unit) : DialogType() + data class ImageCaptureDialog( + val stateId : Int, + val contentType : String + ) : DialogType() data class ImagePickerDialog( val onSelection: (Uri) -> Unit @@ -107,13 +117,16 @@ internal sealed class DialogType { * Shows the appropriate dialogs as requested by the [LocalDialogRequester]. */ @Composable -internal fun FeatureFormDialog() { +internal fun FeatureFormDialog(states : FormStateCollection) { val focusManager = LocalFocusManager.current val dialogRequester = LocalDialogRequester.current val dialogType by dialogRequester.requestFlow.collectAsState() + val context = LocalContext.current + val scope = rememberCoroutineScope() when (dialogType) { is DialogType.ComboBoxDialog -> { - val state = (dialogType as DialogType.ComboBoxDialog).state + val stateId = (dialogType as DialogType.ComboBoxDialog).stateId + val state = states[stateId]!! as CodedValueFieldState ComboBoxDialog( initialValue = state.value.value.data, values = state.codedValues.associateBy({ it.code }, { it.name }), @@ -135,7 +148,8 @@ internal fun FeatureFormDialog() { } is DialogType.DateTimeDialog -> { - val state = (dialogType as DialogType.DateTimeDialog).state + val stateId = (dialogType as DialogType.DateTimeDialog).stateId + val state = states[stateId]!! as DateTimeFieldState val shouldShowTime = remember { state.shouldShowTime } @@ -160,6 +174,7 @@ internal fun FeatureFormDialog() { onDismissRequest = { dialogRequester.dismissDialog() }, onCancelled = { dialogRequester.dismissDialog() }, onConfirmed = { + Log.e("TAG", "FeatureFormDialog: ${state.hashCode()}", ) state.onValueChanged(pickerState.selectedDateTimeMillis?.let { Instant.ofEpochMilli(it) }) @@ -169,10 +184,20 @@ internal fun FeatureFormDialog() { } is DialogType.ImageCaptureDialog -> { - val onImage = (dialogType as DialogType.ImageCaptureDialog).onImage - ImageCapture { - onImage(it) - dialogRequester.dismissDialog() + val stateId = (dialogType as DialogType.ImageCaptureDialog).stateId + Log.e("TAG", "FeatureFormDialog: id is $stateId", ) + val contentType = (dialogType as DialogType.ImageCaptureDialog).contentType + val state = states[stateId]!! as AttachmentElementState + ImageCapture { uri -> + scope.launch { + context.readBytes(uri)?.let { data -> + val name = state.attachments.getNewAttachmentNameForContentType( + contentType + ) + state.addAttachment(name, contentType, data) + } + dialogRequester.dismissDialog() + } } } @@ -193,6 +218,9 @@ internal fun FeatureFormDialog() { } } +internal fun Context.readBytes(uri: Uri): ByteArray? = + contentResolver.openInputStream(uri)?.use { it.buffered().readBytes() } + /** * Computes the [WindowSizeClass] of the device. */ From 18fab1deba0350a14bfcc1f6ef85bbc464de08ab Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Tue, 7 May 2024 11:24:01 -0700 Subject: [PATCH 15/32] added doc --- .../attachment/AttachmentFormElement.kt | 21 ++++++----- .../featureforms/internal/utils/Dialog.kt | 36 +++++++++++++++---- 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt index ca4dc0c0c..786c46a6c 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt @@ -106,7 +106,7 @@ internal fun AttachmentFormElement( label: String, description: String, editable: Boolean, - stateId : Int, + stateId: Int, attachments: List, lazyListState: LazyListState, hasCameraPermission: Boolean, @@ -187,7 +187,7 @@ private fun Header( @Composable private fun AddAttachment( - stateId : Int, + stateId: Int, hasCameraPermission: Boolean, ) { var showMenu by remember { mutableStateOf(false) } @@ -249,17 +249,20 @@ private fun AddAttachment( pickerStyle.collect { when (it) { PickerStyle.Camera -> { - dialogRequester.requestDialog(DialogType.ImageCaptureDialog( - stateId = stateId, - contentType = "image/jpeg" - )) + dialogRequester.requestDialog( + DialogType.ImageCaptureDialog( + stateId = stateId, + contentType = "image/jpeg" + ) + ) } PickerStyle.PickImage -> { dialogRequester.requestDialog( - DialogType.ImagePickerDialog { uri -> - //onAttachment("image/jpeg", uri) - } + DialogType.ImagePickerDialog( + stateId = stateId, + contentType = "image/jpeg" + ) ) } diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt index 19a641868..33e258ad2 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt @@ -92,24 +92,37 @@ internal sealed class DialogType { /** * Indicates a [ComboBoxDialog]. * - * @param state The [CodedValueFieldState] to use for the dialog. + * @param stateId The id of the [CodedValueFieldState] that requested the dialog. */ data class ComboBoxDialog(val stateId: Int) : DialogType() /** * Indicates a [DateTimePicker]. * - * @param state The [DateTimeFieldState] to use for the dialog. + * @param stateId The id of the [DateTimeFieldState] that requested the dialog. */ data class DateTimeDialog(val stateId : Int) : DialogType() + /** + * Indicates an image capture dialog. + * + * @param stateId The id of the [AttachmentElementState] that requested the dialog. + * @param contentType The content type of the image to capture. + */ data class ImageCaptureDialog( val stateId : Int, val contentType : String ) : DialogType() + /** + * Indicates an image picker dialog. + * + * @param stateId The id of the [AttachmentElementState] that requested the dialog. + * @param contentType The content type of the image to pick. + */ data class ImagePickerDialog( - val onSelection: (Uri) -> Unit + val stateId: Int, + val contentType : String ) : DialogType() } @@ -202,10 +215,19 @@ internal fun FeatureFormDialog(states : FormStateCollection) { } is DialogType.ImagePickerDialog -> { - val onMediaPicked = (dialogType as DialogType.ImagePickerDialog).onSelection - ImagePicker { - onMediaPicked(it) - dialogRequester.dismissDialog() + val stateId = (dialogType as DialogType.ImagePickerDialog).stateId + val contentType = (dialogType as DialogType.ImagePickerDialog).contentType + val state = states[stateId]!! as AttachmentElementState + ImagePicker { uri -> + scope.launch { + context.readBytes(uri)?.let { data -> + val name = state.attachments.getNewAttachmentNameForContentType( + contentType + ) + state.addAttachment(name, contentType, data) + } + dialogRequester.dismissDialog() + } } } From 81a82530702fb42d36ca5fbf771a1d77176e95a4 Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Tue, 7 May 2024 11:32:39 -0700 Subject: [PATCH 16/32] fix imports --- .../com/arcgismaps/toolkit/featureforms/FeatureForm.kt | 1 - .../components/attachment/AttachmentElementState.kt | 2 -- .../internal/components/base/BaseFieldState.kt | 2 +- .../internal/components/base/BaseGroupState.kt | 6 +++--- .../internal/components/base/FormElementState.kt | 2 +- .../internal/components/datetime/DateTimeFieldState.kt | 9 +-------- .../internal/components/text/FormTextFieldState.kt | 1 + .../toolkit/featureforms/internal/utils/Dialog.kt | 3 --- 8 files changed, 7 insertions(+), 19 deletions(-) diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/FeatureForm.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/FeatureForm.kt index a54d7dc49..d7da3939b 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/FeatureForm.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/FeatureForm.kt @@ -18,7 +18,6 @@ package com.arcgismaps.toolkit.featureforms -import android.util.Log import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt index e510059d9..258617321 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt @@ -20,7 +20,6 @@ import android.Manifest import android.content.Context import android.content.pm.PackageManager import android.graphics.drawable.BitmapDrawable -import android.util.Log import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.AudioFile @@ -87,7 +86,6 @@ internal class AttachmentElementState( scope.launch { loadAttachments() } - Log.e("TAG", "id: $id, ${hashCode()}", ) } /** diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/base/BaseFieldState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/base/BaseFieldState.kt index cf2fb61d9..bdb9bf4b8 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/base/BaseFieldState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/base/BaseFieldState.kt @@ -76,7 +76,7 @@ internal abstract class BaseFieldState( id = id, label = properties.label, description = properties.description, - isVisible = properties.visible, + isVisible = properties.visible ) { /** * Placeholder hint for the field. diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/base/BaseGroupState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/base/BaseGroupState.kt index c3fce82b1..7b269c51d 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/base/BaseGroupState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/base/BaseGroupState.kt @@ -37,7 +37,7 @@ internal class BaseGroupState( id = id, label = label, description = description, - isVisible = isVisible, + isVisible = isVisible ) { private val _expanded = mutableStateOf(expanded) val expanded: State = _expanded @@ -61,7 +61,7 @@ internal class BaseGroupState( description = groupElement.description, isVisible = groupElement.isVisible, expanded = it, - fieldStates = fieldStates, + fieldStates = fieldStates ) } ) @@ -83,6 +83,6 @@ internal fun rememberBaseGroupState( description = groupElement.description, isVisible = groupElement.isVisible, expanded = groupElement.initialState == FormGroupState.Expanded, - fieldStates = fieldStates, + fieldStates = fieldStates ) } diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/base/FormElementState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/base/FormElementState.kt index a0e2d4a5e..7da1dcf57 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/base/FormElementState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/base/FormElementState.kt @@ -33,5 +33,5 @@ internal abstract class FormElementState( val id : Int, val label : String, val description: String, - val isVisible : StateFlow, + val isVisible : StateFlow ) diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/datetime/DateTimeFieldState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/datetime/DateTimeFieldState.kt index ce0bd5b63..466291dde 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/datetime/DateTimeFieldState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/datetime/DateTimeFieldState.kt @@ -18,7 +18,6 @@ package com.arcgismaps.toolkit.featureforms.internal.components.datetime -import android.util.Log import androidx.compose.runtime.Composable import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.listSaver @@ -31,12 +30,10 @@ import com.arcgismaps.toolkit.featureforms.internal.components.base.BaseFieldSta import com.arcgismaps.toolkit.featureforms.internal.components.base.FieldProperties import com.arcgismaps.toolkit.featureforms.internal.components.base.ValidationErrorState import com.arcgismaps.toolkit.featureforms.internal.components.base.mapValidationErrors -import com.arcgismaps.toolkit.featureforms.internal.components.text.FormTextFieldState -import com.arcgismaps.toolkit.featureforms.internal.components.text.TextFieldProperties import com.arcgismaps.toolkit.featureforms.internal.components.base.mapValueAsStateFlow +import com.arcgismaps.toolkit.featureforms.internal.components.text.TextFieldProperties import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch import java.time.Instant internal class DateTimeFieldProperties( @@ -88,10 +85,6 @@ internal class DateTimeFieldState( val shouldShowTime: Boolean = properties.shouldShowTime override fun typeConverter(input: Instant?): Any? = input - - init { - Log.e("TAG", "id : $label: ${hashCode()}", ) - } companion object { fun Saver( diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/text/FormTextFieldState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/text/FormTextFieldState.kt index 7fabd8146..cb4900217 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/text/FormTextFieldState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/text/FormTextFieldState.kt @@ -57,6 +57,7 @@ internal class TextFieldProperties( * A class to handle the state of a [FormTextField]. Essential properties are inherited from the * [BaseFieldState]. * + * @param id Unique identifier for the field. * @param properties the [TextFieldProperties] associated with this state. * @param initialValue optional initial value to set for this field. It is set to the value of * [TextFieldProperties.value] by default. diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt index 33e258ad2..4f0de85b1 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt @@ -18,7 +18,6 @@ package com.arcgismaps.toolkit.featureforms.internal.utils import android.content.Context import android.net.Uri -import android.util.Log import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.collectAsState @@ -187,7 +186,6 @@ internal fun FeatureFormDialog(states : FormStateCollection) { onDismissRequest = { dialogRequester.dismissDialog() }, onCancelled = { dialogRequester.dismissDialog() }, onConfirmed = { - Log.e("TAG", "FeatureFormDialog: ${state.hashCode()}", ) state.onValueChanged(pickerState.selectedDateTimeMillis?.let { Instant.ofEpochMilli(it) }) @@ -198,7 +196,6 @@ internal fun FeatureFormDialog(states : FormStateCollection) { is DialogType.ImageCaptureDialog -> { val stateId = (dialogType as DialogType.ImageCaptureDialog).stateId - Log.e("TAG", "FeatureFormDialog: id is $stateId", ) val contentType = (dialogType as DialogType.ImageCaptureDialog).contentType val state = states[stateId]!! as AttachmentElementState ImageCapture { uri -> From d6b865f8d5447fe3b7197a9baf3e4ad250616cf0 Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Tue, 7 May 2024 11:36:52 -0700 Subject: [PATCH 17/32] Update AttachmentTile.kt --- .../components/attachment/AttachmentTile.kt | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt index 512c4e9c3..76ef9c12d 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt @@ -17,6 +17,7 @@ package com.arcgismaps.toolkit.featureforms.internal.components.attachment import android.text.format.Formatter +import android.util.Log import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -58,6 +59,8 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -69,6 +72,7 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalViewConfiguration @@ -91,6 +95,7 @@ import com.arcgismaps.toolkit.featureforms.internal.utils.LocalDialogRequester import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import java.lang.ref.WeakReference @Composable internal fun AttachmentTile( @@ -102,8 +107,8 @@ internal fun AttachmentTile( val configuration = LocalViewConfiguration.current val haptic = LocalHapticFeedback.current var showContextMenu by remember { mutableStateOf(false) } - val scope = rememberCoroutineScope() val dialogRequester = LocalDialogRequester.current + val scope = rememberCoroutineScope() Surface( onClick = {}, modifier = Modifier @@ -158,12 +163,8 @@ internal fun AttachmentTile( }, onClick = { showContextMenu = false - dialogRequester.requestDialog(DialogType.RenameAttachmentDialog( - name = state.name, - ) { - scope.launch { - state.renameAttachment(it) - } + dialogRequester.requestDialog(DialogType.RenameAttachmentDialog(state.name) { + state.rename(it) }) }) DropdownMenuItem( @@ -181,9 +182,7 @@ internal fun AttachmentTile( ), onClick = { showContextMenu = false - scope.launch { - state.deleteAttachment() - } + scope.launch { state.delete() } }) } } @@ -209,7 +208,7 @@ internal fun AttachmentTile( // handle single tap if (loadStatus is LoadStatus.NotLoaded || loadStatus is LoadStatus.FailedToLoad) { // load attachment - state.loadAttachment() + state.load() } else if (loadStatus is LoadStatus.Loaded) { // open attachment } @@ -368,9 +367,9 @@ internal fun RenameAttachmentDialog( onRename: (String) -> Unit, onDismissRequest: () -> Unit ) { - val groups = remember(name) { name.split("\\.(?=[^\\\\.]+\$)".toRegex()) } - var filename by remember(groups) { mutableStateOf(groups.first()) } - val extension = remember(groups) { if (groups.count() == 2) groups.last() else "" } + val groups = rememberSaveable(name) { name.split("\\.(?=[^\\\\.]+\$)".toRegex()) } + var filename by rememberSaveable(groups) { mutableStateOf(groups.first()) } + val extension = rememberSaveable(groups) { if (groups.count() == 2) groups.last() else "" } val focusRequester = remember { FocusRequester() } Dialog( onDismissRequest = onDismissRequest, @@ -399,7 +398,10 @@ internal fun RenameAttachmentDialog( onValueChange = { value -> filename = value.text }, modifier = Modifier .fillMaxWidth() - .focusRequester(focusRequester), + .focusRequester(focusRequester) + .onGloballyPositioned { + focusRequester.requestFocus() + }, label = { Text(stringResource(R.string.name)) }, suffix = { Text(text = ".$extension") @@ -425,9 +427,6 @@ internal fun RenameAttachmentDialog( } } ) - LaunchedEffect(Unit) { - focusRequester.requestFocus() - } } @Preview From 963bf0d92da36a722492f3eeb406923e1abf275e Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Tue, 7 May 2024 11:51:00 -0700 Subject: [PATCH 18/32] fixed rename dialog --- .../attachment/AttachmentElementState.kt | 30 +++++++++++-------- .../attachment/AttachmentFormElement.kt | 2 +- .../components/attachment/AttachmentTile.kt | 11 ++++--- .../featureforms/internal/utils/Dialog.kt | 26 +++++++++------- 4 files changed, 40 insertions(+), 29 deletions(-) diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt index 66c36ae3f..69d3265e4 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt @@ -58,10 +58,10 @@ import java.util.Objects * @param evaluateExpressions A method to evaluates the expressions in the form. */ internal class AttachmentElementState( + id: Int, private val formElement: AttachmentFormElement, private val scope: CoroutineScope, - id : Int, - private val evaluateExpressions : suspend () -> Unit + private val evaluateExpressions: suspend () -> Unit ) : FormElementState( label = formElement.label, description = formElement.description, @@ -112,7 +112,7 @@ internal class AttachmentElementState( // refresh the list of attachments loadAttachments() // load the attachment that was just added - attachments.last().loadAttachment() + attachments.last().load() // scroll to the newly added attachment lazyListState.scrollToItem(attachments.size - 1) } @@ -122,7 +122,8 @@ internal class AttachmentElementState( loadAttachments() } - suspend fun renameAttachment(formAttachment: FormAttachment, newName: String) { + suspend fun renameAttachment(name: String, newName: String) { + val formAttachment = formElement.attachments.firstOrNull { it.name == name } ?: return if (formAttachment.name != newName) { formElement.renameAttachment(formAttachment, newName) loadAttachments() @@ -151,12 +152,17 @@ internal class AttachmentElementState( } }, restore = { savedList -> - AttachmentElementState(attachmentFormElement, scope, attachmentFormElement.hashCode() , evaluateExpressions).also { + AttachmentElementState( + id = attachmentFormElement.hashCode(), + formElement = attachmentFormElement, + scope = scope, + evaluateExpressions = evaluateExpressions + ).also { scope.launch { it.loadAttachments() // load the attachments that were previously loaded savedList.forEach { index -> - it.attachments[index].loadAttachment() + it.attachments[index].load() } } } @@ -178,17 +184,17 @@ internal class AttachmentElementState( internal class FormAttachmentState( val name: String, val size: Long, + val elementStateId: Int, val loadStatus: StateFlow, private val onLoadAttachment: suspend () -> Result, private val onLoadThumbnail: suspend () -> Result, val deleteAttachment: suspend () -> Unit, - val renameAttachment: suspend (String) -> Unit, - private val scope: CoroutineScope + private val scope: CoroutineScope, ) { private val _thumbnail: MutableState = mutableStateOf(null) /** - * The thumbnail of the attachment. This is `null` until [loadAttachment] is called. + * The thumbnail of the attachment. This is `null` until [load] is called. */ val thumbnail: State = _thumbnail @@ -204,22 +210,20 @@ internal class FormAttachmentState( ) : this( name = attachment.name, size = attachment.size, + elementStateId = element.id, loadStatus = attachment.loadStatus, onLoadAttachment = attachment::load, onLoadThumbnail = attachment::createFullImage, deleteAttachment = { element.deleteAttachment(attachment) }, - renameAttachment = { - element.renameAttachment(attachment, it) - }, scope = scope ) /** * Loads the attachment and its thumbnail. */ - fun loadAttachment() { + fun load() { scope.launch { onLoadAttachment().onSuccess { onLoadThumbnail().onSuccess { diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt index 7eddb879e..8af341c9f 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt @@ -365,11 +365,11 @@ private fun AttachmentFormElementPreview() { FormAttachmentState( "Photo 1.jpg", 2024, + 1, MutableStateFlow(LoadStatus.Loaded), { Result.success(Unit) }, { Result.success(null) }, {}, - {}, scope = rememberCoroutineScope() ) ), diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt index 76ef9c12d..21ab4a436 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt @@ -163,9 +163,12 @@ internal fun AttachmentTile( }, onClick = { showContextMenu = false - dialogRequester.requestDialog(DialogType.RenameAttachmentDialog(state.name) { - state.rename(it) - }) + dialogRequester.requestDialog( + DialogType.RenameAttachmentDialog( + stateId = state.elementStateId, + name = state.name, + ) + ) }) DropdownMenuItem( text = { Text(text = stringResource(R.string.delete)) }, @@ -182,7 +185,7 @@ internal fun AttachmentTile( ), onClick = { showContextMenu = false - scope.launch { state.delete() } + scope.launch { state.deleteAttachment() } }) } } diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt index 9fa01ce58..6e3b3b0b4 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt @@ -101,7 +101,7 @@ internal sealed class DialogType { * * @param stateId The id of the [DateTimeFieldState] that requested the dialog. */ - data class DateTimeDialog(val stateId : Int) : DialogType() + data class DateTimeDialog(val stateId: Int) : DialogType() /** * Indicates an image capture dialog. @@ -110,8 +110,8 @@ internal sealed class DialogType { * @param contentType The content type of the image to capture. */ data class ImageCaptureDialog( - val stateId : Int, - val contentType : String + val stateId: Int, + val contentType: String ) : DialogType() /** @@ -122,12 +122,13 @@ internal sealed class DialogType { */ data class ImagePickerDialog( val stateId: Int, - val contentType : String + val contentType: String ) : DialogType() + data class RenameAttachmentDialog( - val name : String, - val onRename: (String) -> Unit + val stateId: Int, + val name: String ) : DialogType() } @@ -135,7 +136,7 @@ internal sealed class DialogType { * Shows the appropriate dialogs as requested by the [LocalDialogRequester]. */ @Composable -internal fun FeatureFormDialog(states : FormStateCollection) { +internal fun FeatureFormDialog(states: FormStateCollection) { val focusManager = LocalFocusManager.current val dialogRequester = LocalDialogRequester.current val dialogType by dialogRequester.requestFlow.collectAsState() @@ -235,13 +236,16 @@ internal fun FeatureFormDialog(states : FormStateCollection) { } is DialogType.RenameAttachmentDialog -> { - val onRenameAttachment = (dialogType as DialogType.RenameAttachmentDialog).onRename + val stateId = (dialogType as DialogType.RenameAttachmentDialog).stateId val name = (dialogType as DialogType.RenameAttachmentDialog).name + val state = states[stateId]!! as AttachmentElementState RenameAttachmentDialog( name = name, - onRename = { - onRenameAttachment(it) - dialogRequester.dismissDialog() + onRename = { newName -> + scope.launch { + state.renameAttachment(name, newName) + dialogRequester.dismissDialog() + } } ) { dialogRequester.dismissDialog() From 0d679b232cc1fbf6adf13d0ff5c185684aabf40d Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Wed, 8 May 2024 08:51:33 -0700 Subject: [PATCH 19/32] fix conficts --- .../internal/components/attachment/AttachmentTile.kt | 11 +++++++---- .../toolkit/featureforms/internal/utils/Dialog.kt | 8 ++++++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt index 21ab4a436..295cd807d 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt @@ -17,7 +17,6 @@ package com.arcgismaps.toolkit.featureforms.internal.components.attachment import android.text.format.Formatter -import android.util.Log import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -37,6 +36,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.ArrowDownward import androidx.compose.material.icons.outlined.Delete @@ -59,7 +59,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -80,6 +79,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -95,7 +95,6 @@ import com.arcgismaps.toolkit.featureforms.internal.utils.LocalDialogRequester import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import java.lang.ref.WeakReference @Composable internal fun AttachmentTile( @@ -408,7 +407,11 @@ internal fun RenameAttachmentDialog( label = { Text(stringResource(R.string.name)) }, suffix = { Text(text = ".$extension") - } + }, + singleLine = true, + keyboardOptions = KeyboardOptions.Default.copy( + imeAction = ImeAction.Done, + ) ) Spacer(modifier = Modifier.height(25.dp)) Row( diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt index 49fa7c4d9..5bcc832ba 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt @@ -125,11 +125,15 @@ internal sealed class DialogType { val contentType: String ) : DialogType() - + /** + * Indicates a dialog to rename an attachment. + * + * @param stateId The id of the [AttachmentElementState] that requested the dialog. + * @param name The current name of the attachment. + */ data class RenameAttachmentDialog( val stateId: Int, val name: String, - val contentType: String ) : DialogType() } From d981e45d868ce680941fa5489ecb44114792d7ab Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Wed, 8 May 2024 11:35:47 -0700 Subject: [PATCH 20/32] init working --- .../featureforms/src/main/AndroidManifest.xml | 6 +- .../attachment/AttachmentElementState.kt | 81 ++++++++++++++----- .../attachment/AttachmentFormElement.kt | 15 ++-- .../components/attachment/AttachmentTile.kt | 16 ++++ .../utils/AttachmentCaptureFileProvider.kt | 43 ---------- .../internal/utils/AttachmentsFileProvider.kt | 63 +++++++++++++++ ...ents.xml => feature_forms_attachments.xml} | 2 +- 7 files changed, 154 insertions(+), 72 deletions(-) delete mode 100644 toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/AttachmentCaptureFileProvider.kt create mode 100644 toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/AttachmentsFileProvider.kt rename toolkit/featureforms/src/main/res/xml/{feature_forms_captured_attachments.xml => feature_forms_attachments.xml} (93%) diff --git a/toolkit/featureforms/src/main/AndroidManifest.xml b/toolkit/featureforms/src/main/AndroidManifest.xml index eb3a2c57f..0fb810980 100644 --- a/toolkit/featureforms/src/main/AndroidManifest.xml +++ b/toolkit/featureforms/src/main/AndroidManifest.xml @@ -20,13 +20,13 @@ + android:resource="@xml/feature_forms_attachments" /> diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt index 26a439630..7980fe531 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt @@ -20,6 +20,7 @@ import android.Manifest import android.content.Context import android.content.pm.PackageManager import android.graphics.drawable.BitmapDrawable +import android.net.Uri import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.AudioFile @@ -39,6 +40,7 @@ import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext import androidx.core.content.ContextCompat import com.arcgismaps.LoadStatus import com.arcgismaps.mapping.featureforms.AttachmentFormElement @@ -46,8 +48,11 @@ import com.arcgismaps.mapping.featureforms.FeatureForm import com.arcgismaps.mapping.featureforms.FormAttachment import com.arcgismaps.toolkit.featureforms.internal.components.base.FormElementState import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch +import java.io.File +import java.io.FileOutputStream import java.util.Objects /** @@ -61,7 +66,8 @@ internal class AttachmentElementState( id: Int, private val formElement: AttachmentFormElement, private val scope: CoroutineScope, - private val evaluateExpressions: suspend () -> Unit + private val evaluateExpressions: suspend () -> Unit, + private val filesDir : String ) : FormElementState( id = id, label = formElement.label, @@ -98,7 +104,7 @@ internal class AttachmentElementState( attachments.clear() attachments.addAll( formElement.attachments.map { - FormAttachmentState(this, it, scope) + FormAttachmentState(this, it, scope, filesDir) } ) } @@ -112,7 +118,7 @@ internal class AttachmentElementState( // refresh the list of attachments loadAttachments() // load the attachment that was just added - attachments.last().load() + attachments.last().loadThumbnail() // scroll to the newly added attachment lazyListState.scrollToItem(attachments.size - 1) } @@ -139,7 +145,8 @@ internal class AttachmentElementState( fun Saver( attachmentFormElement: AttachmentFormElement, scope: CoroutineScope, - evaluateExpressions: suspend () -> Unit + evaluateExpressions: suspend () -> Unit, + filesDir: String ): Saver = listSaver( save = { // save the list of indices of attachments that have been loaded @@ -156,13 +163,14 @@ internal class AttachmentElementState( id = attachmentFormElement.hashCode(), formElement = attachmentFormElement, scope = scope, - evaluateExpressions = evaluateExpressions + evaluateExpressions = evaluateExpressions, + filesDir = filesDir ).also { scope.launch { it.loadAttachments() // load the attachments that were previously loaded savedList.forEach { index -> - it.attachments[index].load() + it.attachments[index].loadThumbnail() } } } @@ -184,12 +192,14 @@ internal class AttachmentElementState( internal class FormAttachmentState( val name: String, val size: Long, + val contentType : String, val elementStateId: Int, val loadStatus: StateFlow, - private val onLoadAttachment: suspend () -> Result, + private val onLoadAttachment: suspend () -> Result, private val onLoadThumbnail: suspend () -> Result, val deleteAttachment: suspend () -> Unit, private val scope: CoroutineScope, + private val filesDir : String, ) { private val _thumbnail: MutableState = mutableStateOf(null) @@ -198,6 +208,12 @@ internal class FormAttachmentState( */ val thumbnail: State = _thumbnail + var uri : Uri = Uri.EMPTY + private set + + var filePath: String = "" + private set + /** * The type of the attachment. */ @@ -206,35 +222,61 @@ internal class FormAttachmentState( constructor( element: AttachmentElementState, attachment: FormAttachment, - scope: CoroutineScope + scope: CoroutineScope, + filesDir : String, ) : this( name = attachment.name, size = attachment.size, + contentType = attachment.contentType, elementStateId = element.id, loadStatus = attachment.loadStatus, - onLoadAttachment = attachment::load, + onLoadAttachment = { + attachment.load() + attachment.attachment?.fetchData() + ?: Result.failure(Exception("Attachment data is null")) + }, onLoadThumbnail = attachment::createFullImage, deleteAttachment = { element.deleteAttachment(attachment) }, - scope = scope + scope = scope, + filesDir = filesDir ) /** * Loads the attachment and its thumbnail. */ fun load() { - scope.launch { - onLoadAttachment().onSuccess { - onLoadThumbnail().onSuccess { - if (it != null) { - _thumbnail.value = it.bitmap.asImageBitmap() - } + scope.launch(Dispatchers.IO) { + onLoadAttachment().onSuccess { data -> + if (data != null) { + writeDataToDisk(data) } + loadThumbnail() + } + } + } + + suspend fun loadThumbnail() { + onLoadThumbnail().onSuccess { + if (it != null) { + _thumbnail.value = it.bitmap.asImageBitmap() } } } + private fun writeDataToDisk(data: ByteArray) { + val directory = File(filesDir, "feature_forms_attachments") + directory.mkdirs() + // write the data to disk + val file = File(directory, name) + file.createNewFile() + FileOutputStream(file).use { + it.write(data) + } + filePath = file.absolutePath + } + override fun hashCode(): Int { return Objects.hash(name, size, type) } @@ -259,19 +301,22 @@ internal fun rememberAttachmentElementState( attachmentFormElement: AttachmentFormElement ): AttachmentElementState { val scope = rememberCoroutineScope() + val context = LocalContext.current return rememberSaveable( inputs = arrayOf(form), saver = AttachmentElementState.Saver( attachmentFormElement, scope, - form::evaluateExpressions + form::evaluateExpressions, + context.cacheDir.absolutePath ) ) { AttachmentElementState( formElement = attachmentFormElement, scope = scope, id = attachmentFormElement.hashCode(), - evaluateExpressions = form::evaluateExpressions + evaluateExpressions = form::evaluateExpressions, + filesDir = context.cacheDir.absolutePath ) } } diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt index 8af341c9f..6d0ff6f73 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt @@ -66,10 +66,9 @@ import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import com.arcgismaps.LoadStatus import com.arcgismaps.toolkit.featureforms.R -import com.arcgismaps.toolkit.featureforms.internal.utils.AttachmentCaptureFileProvider +import com.arcgismaps.toolkit.featureforms.internal.utils.AttachmentsFileProvider import com.arcgismaps.toolkit.featureforms.internal.utils.DialogType import com.arcgismaps.toolkit.featureforms.internal.utils.LocalDialogRequester -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch @@ -150,7 +149,7 @@ private fun Carousel(state: LazyListState, attachments: List Unit) { restore = { Uri.parse(it.first()) } ) ) { - val file = context.createTempImageFile() - AttachmentCaptureFileProvider.getImageUri(file, context) + val timeStamp = Instant.now().toEpochMilli() + AttachmentsFileProvider.createTempFileWithUri("IMAGE_$timeStamp", ".jpg", context) } val cameraLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.TakePicture(), @@ -365,12 +364,14 @@ private fun AttachmentFormElementPreview() { FormAttachmentState( "Photo 1.jpg", 2024, + "image/jpeg", 1, MutableStateFlow(LoadStatus.Loaded), - { Result.success(Unit) }, + { Result.success(null) }, { Result.success(null) }, {}, - scope = rememberCoroutineScope() + scope = rememberCoroutineScope(), + "" ) ), lazyListState = LazyListState(), diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt index 295cd807d..665649f33 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt @@ -16,6 +16,7 @@ package com.arcgismaps.toolkit.featureforms.internal.components.attachment +import android.content.Intent import android.text.format.Formatter import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image @@ -90,11 +91,13 @@ import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import com.arcgismaps.LoadStatus import com.arcgismaps.toolkit.featureforms.R +import com.arcgismaps.toolkit.featureforms.internal.utils.AttachmentsFileProvider import com.arcgismaps.toolkit.featureforms.internal.utils.DialogType import com.arcgismaps.toolkit.featureforms.internal.utils.LocalDialogRequester import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import java.io.File @Composable internal fun AttachmentTile( @@ -108,6 +111,7 @@ internal fun AttachmentTile( var showContextMenu by remember { mutableStateOf(false) } val dialogRequester = LocalDialogRequester.current val scope = rememberCoroutineScope() + val context = LocalContext.current Surface( onClick = {}, modifier = Modifier @@ -213,6 +217,18 @@ internal fun AttachmentTile( state.load() } else if (loadStatus is LoadStatus.Loaded) { // open attachment + val intent = Intent() + intent.setAction(Intent.ACTION_VIEW) + val uri = AttachmentsFileProvider.getUriForFile( + context = context, + file = File(state.filePath) + ) + intent.setDataAndType( + uri, + state.contentType + ) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + context.startActivity(intent) } } } diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/AttachmentCaptureFileProvider.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/AttachmentCaptureFileProvider.kt deleted file mode 100644 index 42b592948..000000000 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/AttachmentCaptureFileProvider.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2024 Esri - * - * 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 - * - * http://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 com.arcgismaps.toolkit.featureforms.internal.utils - -import android.content.Context -import android.net.Uri -import androidx.core.content.FileProvider -import com.arcgismaps.toolkit.featureforms.R -import java.io.File - -internal class AttachmentCaptureFileProvider : - FileProvider(R.xml.feature_forms_captured_attachments) { - companion object { - private const val AUTHORITY = "com.arcgismaps.toolkit.featureforms.capturefileprovider" - - fun getImageUri(file: File, context: Context): Uri { - val directory = File(context.cacheDir, "feature_forms_attachments") - directory.mkdirs() - // The authority string must be unique per device. Therefore use of this provider with two - // installations of the featureforms dependency on one device will crash. - // The solution is to release a standalone file provider aidl service which both instances can use. - return getUriForFile( - context, - AUTHORITY, - file, - ) - } - } -} diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/AttachmentsFileProvider.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/AttachmentsFileProvider.kt new file mode 100644 index 000000000..4f85ef876 --- /dev/null +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/AttachmentsFileProvider.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2024 Esri + * + * 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 + * + * http://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 com.arcgismaps.toolkit.featureforms.internal.utils + +import android.content.Context +import android.net.IpPrefix +import android.net.Uri +import androidx.core.content.FileProvider +import com.arcgismaps.toolkit.featureforms.R +import java.io.File + +internal class AttachmentsFileProvider : + FileProvider(R.xml.feature_forms_attachments) { + companion object { + + private const val AUTHORITY_BASE = "com.arcgismaps.toolkit.featureforms.attachmentsfileprovider" + private const val FILE_PROVIDER_PATH = "feature_forms_attachments" + + fun createFileWithUri(name: String, context: Context): Uri { + val authority = "${context.packageName}.$AUTHORITY_BASE" + val directory = File(context.cacheDir, AUTHORITY_BASE) + directory.mkdirs() + val file = File(directory, name) + file.createNewFile() + return getUriForFile( + context, + authority, + file, + ) + } + + fun createTempFileWithUri(prefix: String, suffix: String, context: Context): Uri { + val authority = "${context.packageName}.$AUTHORITY_BASE" + val directory = File(context.cacheDir, FILE_PROVIDER_PATH) + directory.mkdirs() + val file = File.createTempFile(prefix, suffix, directory) + return getUriForFile( + context, + AUTHORITY_BASE, + file, + ) + } + + fun getUriForFile(file: File, context: Context): Uri { + val authority = "${context.packageName}.$AUTHORITY_BASE" + return getUriForFile(context, AUTHORITY_BASE, file) + } + } +} diff --git a/toolkit/featureforms/src/main/res/xml/feature_forms_captured_attachments.xml b/toolkit/featureforms/src/main/res/xml/feature_forms_attachments.xml similarity index 93% rename from toolkit/featureforms/src/main/res/xml/feature_forms_captured_attachments.xml rename to toolkit/featureforms/src/main/res/xml/feature_forms_attachments.xml index a87b5ada1..9853b2ac2 100644 --- a/toolkit/featureforms/src/main/res/xml/feature_forms_captured_attachments.xml +++ b/toolkit/featureforms/src/main/res/xml/feature_forms_attachments.xml @@ -18,6 +18,6 @@ --> From 88ca86154d71aa74e5fc069af81f0fa59521e7df Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Wed, 8 May 2024 14:12:41 -0700 Subject: [PATCH 21/32] update authority --- toolkit/featureforms/src/main/AndroidManifest.xml | 2 +- .../featureforms/internal/utils/AttachmentsFileProvider.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/toolkit/featureforms/src/main/AndroidManifest.xml b/toolkit/featureforms/src/main/AndroidManifest.xml index 0fb810980..24fc9f67a 100644 --- a/toolkit/featureforms/src/main/AndroidManifest.xml +++ b/toolkit/featureforms/src/main/AndroidManifest.xml @@ -21,7 +21,7 @@ Date: Thu, 9 May 2024 18:56:21 -0700 Subject: [PATCH 22/32] fix capture uri and delete workaround --- .../attachment/AttachmentElementState.kt | 3 ++- .../internal/utils/AttachmentsFileProvider.kt | 17 ++--------------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt index 7980fe531..bbdeaa60f 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt @@ -125,7 +125,8 @@ internal class AttachmentElementState( suspend fun deleteAttachment(formAttachment: FormAttachment) { formElement.deleteAttachment(formAttachment) - loadAttachments() + val state = attachments.find { it.name == formAttachment.name } ?: return + attachments.remove(state) } suspend fun renameAttachment(name: String, newName: String) { diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/AttachmentsFileProvider.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/AttachmentsFileProvider.kt index a5f5420ca..40ff863de 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/AttachmentsFileProvider.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/AttachmentsFileProvider.kt @@ -30,27 +30,14 @@ internal class AttachmentsFileProvider : private const val AUTHORITY_BASE = "com.arcgismaps.toolkit.featureforms.attachmentsfileprovider" private const val FILE_PROVIDER_PATH = "feature_forms_attachments" - fun createFileWithUri(name: String, context: Context): Uri { - val authority = "${context.packageName}.$AUTHORITY_BASE" - val directory = File(context.cacheDir, AUTHORITY_BASE) - directory.mkdirs() - val file = File(directory, name) - file.createNewFile() - return getUriForFile( - context, - authority, - file, - ) - } - fun createTempFileWithUri(prefix: String, suffix: String, context: Context): Uri { val authority = "${context.packageName}.$AUTHORITY_BASE" - val directory = File(context.cacheDir, authority) + val directory = File(context.cacheDir, FILE_PROVIDER_PATH) directory.mkdirs() val file = File.createTempFile(prefix, suffix, directory) return getUriForFile( context, - AUTHORITY_BASE, + authority, file, ) } From 13ad0999a24cce48492ce20c4acecfb66c5e1154 Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Fri, 10 May 2024 10:59:42 -0700 Subject: [PATCH 23/32] make formattachmentstate loadable --- .../attachment/AttachmentElementState.kt | 188 +++++++++++------- .../attachment/AttachmentFormElement.kt | 34 +--- .../components/attachment/AttachmentTile.kt | 2 +- .../internal/utils/AttachmentsFileProvider.kt | 5 +- 4 files changed, 127 insertions(+), 102 deletions(-) diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt index bbdeaa60f..ba994b77a 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt @@ -19,8 +19,6 @@ package com.arcgismaps.toolkit.featureforms.internal.components.attachment import android.Manifest import android.content.Context import android.content.pm.PackageManager -import android.graphics.drawable.BitmapDrawable -import android.net.Uri import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.AudioFile @@ -43,14 +41,19 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.core.content.ContextCompat import com.arcgismaps.LoadStatus +import com.arcgismaps.Loadable import com.arcgismaps.mapping.featureforms.AttachmentFormElement import com.arcgismaps.mapping.featureforms.FeatureForm import com.arcgismaps.mapping.featureforms.FormAttachment import com.arcgismaps.toolkit.featureforms.internal.components.base.FormElementState +import com.arcgismaps.toolkit.featureforms.internal.utils.AttachmentsFileProvider +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.io.File import java.io.FileOutputStream import java.util.Objects @@ -67,7 +70,7 @@ internal class AttachmentElementState( private val formElement: AttachmentFormElement, private val scope: CoroutineScope, private val evaluateExpressions: suspend () -> Unit, - private val filesDir : String + private val filesDir: String ) : FormElementState( id = id, label = formElement.label, @@ -101,10 +104,21 @@ internal class AttachmentElementState( */ private suspend fun loadAttachments() { formElement.fetchAttachments() - attachments.clear() + formElement.attachments.forEach { formAttachment -> + + } attachments.addAll( formElement.attachments.map { - FormAttachmentState(this, it, scope, filesDir) + FormAttachmentState( + name = it.name, + size = it.size, + contentType = it.contentType, + elementStateId = id, + deleteAttachment = { deleteAttachment(it) }, + filesDir = filesDir, + scope = scope, + formAttachment = it + ) } ) } @@ -118,12 +132,12 @@ internal class AttachmentElementState( // refresh the list of attachments loadAttachments() // load the attachment that was just added - attachments.last().loadThumbnail() + attachments.last().loadWithParentScope() // scroll to the newly added attachment lazyListState.scrollToItem(attachments.size - 1) } - suspend fun deleteAttachment(formAttachment: FormAttachment) { + private suspend fun deleteAttachment(formAttachment: FormAttachment) { formElement.deleteAttachment(formAttachment) val state = attachments.find { it.name == formAttachment.name } ?: return attachments.remove(state) @@ -157,6 +171,9 @@ internal class AttachmentElementState( add(i) } } + // save the index of the first visible item + add(it.lazyListState.firstVisibleItemIndex) + add(it.lazyListState.firstVisibleItemScrollOffset) } }, restore = { savedList -> @@ -170,9 +187,13 @@ internal class AttachmentElementState( scope.launch { it.loadAttachments() // load the attachments that were previously loaded - savedList.forEach { index -> - it.attachments[index].loadThumbnail() + for (i in savedList.dropLast(2)) { + it.attachments[i].loadWithParentScope() } + // scroll to the last visible item + val firstVisibleItemIndex = savedList[savedList.count() - 2] + val firstVisibleItemScrollOffset = savedList[savedList.count() - 1] + it.lazyListState.scrollToItem(firstVisibleItemIndex, firstVisibleItemScrollOffset) } } } @@ -185,23 +206,24 @@ internal class AttachmentElementState( * * @param name The name of the attachment. * @param size The size of the attachment. - * @param loadStatus The load status of the attachment. - * @param onLoadAttachment A function that loads the attachment. - * @param onLoadThumbnail A function that loads the thumbnail of the attachment. + * @param contentType The content type of the attachment. + * @param elementStateId The ID of the [AttachmentElementState] that created this attachment. + * @param deleteAttachment A function to delete the attachment. + * @param filesDir The directory where the attachments are stored. * @param scope The coroutine scope used to launch coroutines. + * @param formAttachment The [FormAttachment] that this state represents. */ internal class FormAttachmentState( val name: String, val size: Long, - val contentType : String, + val contentType: String, val elementStateId: Int, - val loadStatus: StateFlow, - private val onLoadAttachment: suspend () -> Result, - private val onLoadThumbnail: suspend () -> Result, val deleteAttachment: suspend () -> Unit, + private val filesDir: String, private val scope: CoroutineScope, - private val filesDir : String, -) { + private val formAttachment: FormAttachment? = null +) : Loadable { + private val _thumbnail: MutableState = mutableStateOf(null) /** @@ -209,73 +231,89 @@ internal class FormAttachmentState( */ val thumbnail: State = _thumbnail - var uri : Uri = Uri.EMPTY - private set + /** + * The type of the attachment. + */ + val type: AttachmentType = getAttachmentType(name) + + private val _loadStatus: MutableStateFlow = MutableStateFlow(LoadStatus.NotLoaded) + override val loadStatus = _loadStatus.asStateFlow() + /** + * The file path of the attachment on disk. This is empty until [load] is called. + */ var filePath: String = "" private set /** - * The type of the attachment. + * The directory where the attachments are stored as defined in the [AttachmentsFileProvider]. */ - val type: AttachmentType = getAttachmentType(name) + private val attachmentsDir = "feature_forms_attachments" - constructor( - element: AttachmentElementState, - attachment: FormAttachment, - scope: CoroutineScope, - filesDir : String, - ) : this( - name = attachment.name, - size = attachment.size, - contentType = attachment.contentType, - elementStateId = element.id, - loadStatus = attachment.loadStatus, - onLoadAttachment = { - attachment.load() - attachment.attachment?.fetchData() - ?: Result.failure(Exception("Attachment data is null")) - }, - onLoadThumbnail = attachment::createFullImage, - deleteAttachment = { - element.deleteAttachment(attachment) - }, - scope = scope, - filesDir = filesDir - ) + /** + * Loads the attachment and its thumbnail in the coroutine scope of the state object that + * created this attachment. Usually, this is the [AttachmentElementState] that created this + * within the CoroutineScope of the root Feature Form composable. + */ + fun loadWithParentScope() { + scope.launch { + load() + } + } /** - * Loads the attachment and its thumbnail. + * Loads the attachment and its thumbnail. Use [loadWithParentScope] to load the attachment as + * a long-running task. This coroutine will get cancelled if the calling composable is removed + * from the composition. */ - fun load() { - scope.launch(Dispatchers.IO) { - onLoadAttachment().onSuccess { data -> - if (data != null) { - writeDataToDisk(data) + override suspend fun load(): Result { + _loadStatus.value = LoadStatus.Loading + var result = Result.success(Unit) + try { + if (formAttachment == null) { + result = Result.failure(Exception("Form attachment is null")) + } else { + formAttachment.retryLoad().onFailure { + result = Result.failure(it) + }.onSuccess { + val data = formAttachment.attachment?.fetchData()?.getOrNull() + if (data != null) { + formAttachment.createFullImage().onSuccess { + _thumbnail.value = it.bitmap.asImageBitmap() + } + // write the data to disk only if the file does not exist + if (!File(filePath).exists()) { + writeDataToDisk(data) + } + } else { + result = Result.failure(Exception("Failed to load attachment data")) + } } - loadThumbnail() } + } catch (ex: CancellationException) { + result = Result.failure(ex) + throw ex + } catch (ex : Exception) { + result = Result.failure(ex) } - } - - suspend fun loadThumbnail() { - onLoadThumbnail().onSuccess { - if (it != null) { - _thumbnail.value = it.bitmap.asImageBitmap() + finally { + if (result.isSuccess) { + _loadStatus.value = LoadStatus.Loaded + } else { + val error = result.exceptionOrNull() ?: Exception("Failed to load attachment") + _loadStatus.value = LoadStatus.FailedToLoad(error) } } + return result } - private fun writeDataToDisk(data: ByteArray) { - val directory = File(filesDir, "feature_forms_attachments") - directory.mkdirs() - // write the data to disk - val file = File(directory, name) - file.createNewFile() - FileOutputStream(file).use { - it.write(data) - } - filePath = file.absolutePath + override fun cancelLoad() { + formAttachment?.cancelLoad() + } + + override suspend fun retryLoad(): Result { + return formAttachment?.retryLoad() + ?: return Result.failure(Exception("Form attachment is null")) } override fun hashCode(): Int { @@ -294,6 +332,18 @@ internal class FormAttachmentState( return true } + + private suspend fun writeDataToDisk(data: ByteArray) = withContext(Dispatchers.IO) { + val directory = File(filesDir, attachmentsDir) + directory.mkdirs() + // write the data to disk + val file = File(directory, name) + file.createNewFile() + FileOutputStream(file).use { + it.write(data) + } + filePath = file.absolutePath + } } @Composable diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt index 6d0ff6f73..c3062d066 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt @@ -16,7 +16,6 @@ package com.arcgismaps.toolkit.featureforms.internal.components.attachment -import android.content.Context import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest @@ -64,15 +63,12 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp -import com.arcgismaps.LoadStatus import com.arcgismaps.toolkit.featureforms.R import com.arcgismaps.toolkit.featureforms.internal.utils.AttachmentsFileProvider import com.arcgismaps.toolkit.featureforms.internal.utils.DialogType import com.arcgismaps.toolkit.featureforms.internal.utils.LocalDialogRequester import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch -import java.io.File import java.time.Instant @Composable @@ -80,7 +76,6 @@ internal fun AttachmentFormElement( state: AttachmentElementState, modifier: Modifier = Modifier ) { - val scope = rememberCoroutineScope() val context = LocalContext.current val editable by state.isEditable.collectAsState() AttachmentFormElement( @@ -91,11 +86,6 @@ internal fun AttachmentFormElement( attachments = state.attachments, lazyListState = state.lazyListState, hasCameraPermission = state.hasCameraPermissions(context), - onAttachmentAdded = { name, contentType, data -> - scope.launch { - state.addAttachment(name, contentType, data) - } - }, modifier = modifier ) } @@ -109,12 +99,9 @@ internal fun AttachmentFormElement( attachments: List, lazyListState: LazyListState, hasCameraPermission: Boolean, - onAttachmentAdded: suspend (String, String, ByteArray) -> Unit, modifier: Modifier = Modifier, colors: AttachmentElementColors = AttachmentElementDefaults.colors() ) { - val context = LocalContext.current - val scope = rememberCoroutineScope() Card( modifier = modifier, shape = AttachmentElementDefaults.containerShape, @@ -149,7 +136,7 @@ private fun Carousel(state: LazyListState, attachments: List.getNewAttachmentNameForContentType( return "$attachmentType $count.$ext" } -internal fun Context.createTempImageFile(): File { - val timeStamp = Instant.now().toEpochMilli() - val dir = File(cacheDir, "feature_forms_attachments") - dir.mkdirs() - return File.createTempFile( - "IMAGE_$timeStamp", - ".jpg", - dir, - ) -} - private sealed class PickerStyle { data object File : PickerStyle() data object Camera : PickerStyle() @@ -366,16 +342,12 @@ private fun AttachmentFormElementPreview() { 2024, "image/jpeg", 1, - MutableStateFlow(LoadStatus.Loaded), - { Result.success(null) }, - { Result.success(null) }, {}, - scope = rememberCoroutineScope(), - "" + "", + scope = rememberCoroutineScope() ) ), lazyListState = LazyListState(), hasCameraPermission = true, - onAttachmentAdded = { _, _, _ -> } ) } diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt index 665649f33..40d38f05d 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt @@ -214,7 +214,7 @@ internal fun AttachmentTile( // handle single tap if (loadStatus is LoadStatus.NotLoaded || loadStatus is LoadStatus.FailedToLoad) { // load attachment - state.load() + state.loadWithParentScope() } else if (loadStatus is LoadStatus.Loaded) { // open attachment val intent = Intent() diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/AttachmentsFileProvider.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/AttachmentsFileProvider.kt index 40ff863de..4ab2026c8 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/AttachmentsFileProvider.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/AttachmentsFileProvider.kt @@ -17,7 +17,6 @@ package com.arcgismaps.toolkit.featureforms.internal.utils import android.content.Context -import android.net.IpPrefix import android.net.Uri import androidx.core.content.FileProvider import com.arcgismaps.toolkit.featureforms.R @@ -31,6 +30,8 @@ internal class AttachmentsFileProvider : private const val FILE_PROVIDER_PATH = "feature_forms_attachments" fun createTempFileWithUri(prefix: String, suffix: String, context: Context): Uri { + // authority is unique, which uses the package name + base authority name + // to avoid conflicts with other apps using the same library val authority = "${context.packageName}.$AUTHORITY_BASE" val directory = File(context.cacheDir, FILE_PROVIDER_PATH) directory.mkdirs() @@ -43,6 +44,8 @@ internal class AttachmentsFileProvider : } fun getUriForFile(file: File, context: Context): Uri { + // authority is unique, which uses the package name + base authority name + // to avoid conflicts with other apps using the same library val authority = "${context.packageName}.$AUTHORITY_BASE" return getUriForFile(context, authority, file) } From f3d2fdd5048155f5fc450b89fa693224b69680d0 Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Fri, 10 May 2024 11:05:35 -0700 Subject: [PATCH 24/32] Update AttachmentElementState.kt --- .../internal/components/attachment/AttachmentElementState.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt index ba994b77a..7ce3d6f39 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt @@ -104,9 +104,7 @@ internal class AttachmentElementState( */ private suspend fun loadAttachments() { formElement.fetchAttachments() - formElement.attachments.forEach { formAttachment -> - - } + attachments.clear() attachments.addAll( formElement.attachments.map { FormAttachmentState( From d491c8fdfc4187e3d4acde88888e8c6911cc50ea Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Mon, 13 May 2024 15:27:33 -0700 Subject: [PATCH 25/32] fixed attachments recreation --- .../attachment/AttachmentElementState.kt | 113 +++++++++++++----- .../attachment/AttachmentFormElement.kt | 51 ++++---- .../featureforms/internal/utils/Dialog.kt | 10 +- 3 files changed, 110 insertions(+), 64 deletions(-) diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt index 7ce3d6f39..d8ece8335 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt @@ -29,12 +29,13 @@ import androidx.compose.material.icons.outlined.VideoCameraBack import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.State +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.listSaver import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.vector.ImageVector @@ -80,7 +81,7 @@ internal class AttachmentElementState( /** * The attachments associated with the form element. */ - val attachments = SnapshotStateList() + var attachments by mutableStateOf(emptyMap()) /** * Indicates whether the attachment form element is editable. @@ -104,21 +105,31 @@ internal class AttachmentElementState( */ private suspend fun loadAttachments() { formElement.fetchAttachments() - attachments.clear() - attachments.addAll( - formElement.attachments.map { - FormAttachmentState( - name = it.name, - size = it.size, - contentType = it.contentType, - elementStateId = id, - deleteAttachment = { deleteAttachment(it) }, - filesDir = filesDir, - scope = scope, - formAttachment = it - ) + attachments = buildMap { + formElement.attachments.forEach { formAttachment -> + val state = attachments[formAttachment.hashCode()] + if (state == null) { + // if does not exist + // then add it to the list + FormAttachmentState( + name = formAttachment.name, + size = formAttachment.size, + contentType = formAttachment.contentType, + elementStateId = id, + deleteAttachment = { deleteAttachment(formAttachment) }, + filesDir = filesDir, + scope = scope, + formAttachment = formAttachment + ).also { newState -> + put(formAttachment.hashCode(), newState) + } + } else { + // update the current state + state.update(formAttachment) + put(formAttachment.hashCode(), state) + } } - ) + } } /** @@ -128,17 +139,30 @@ internal class AttachmentElementState( formElement.addAttachment(name, contentType, data) evaluateExpressions() // refresh the list of attachments - loadAttachments() - // load the attachment that was just added - attachments.last().loadWithParentScope() - // scroll to the newly added attachment - lazyListState.scrollToItem(attachments.size - 1) + attachments = buildMap { + // add the new attachment to the front of the list + val formAttachment = formElement.attachments.last() + FormAttachmentState( + name = formAttachment.name, + size = formAttachment.size, + contentType = formAttachment.contentType, + elementStateId = id, + deleteAttachment = { deleteAttachment(formAttachment) }, + filesDir = filesDir, + scope = scope, + formAttachment = formAttachment + ).also { newState -> + // load the attachment that was just added + newState.loadWithParentScope() + put(formAttachment.hashCode(), newState) + } + putAll(attachments) + } } private suspend fun deleteAttachment(formAttachment: FormAttachment) { formElement.deleteAttachment(formAttachment) - val state = attachments.find { it.name == formAttachment.name } ?: return - attachments.remove(state) + attachments = attachments.filter { it.key != formAttachment.hashCode() } } suspend fun renameAttachment(name: String, newName: String) { @@ -162,11 +186,11 @@ internal class AttachmentElementState( filesDir: String ): Saver = listSaver( save = { - // save the list of indices of attachments that have been loaded buildList { - for (i in it.attachments.indices) { - if (it.attachments[i].loadStatus.value is LoadStatus.Loaded) { - add(i) + // save the keys of attachments that have been loaded + it.attachments.forEach { entry -> + if (entry.value.loadStatus.value is LoadStatus.Loaded) { + add(entry.key) } } // save the index of the first visible item @@ -186,12 +210,15 @@ internal class AttachmentElementState( it.loadAttachments() // load the attachments that were previously loaded for (i in savedList.dropLast(2)) { - it.attachments[i].loadWithParentScope() + it.attachments[i]?.loadWithParentScope() } // scroll to the last visible item val firstVisibleItemIndex = savedList[savedList.count() - 2] val firstVisibleItemScrollOffset = savedList[savedList.count() - 1] - it.lazyListState.scrollToItem(firstVisibleItemIndex, firstVisibleItemScrollOffset) + it.lazyListState.scrollToItem( + firstVisibleItemIndex, + firstVisibleItemScrollOffset + ) } } } @@ -212,7 +239,7 @@ internal class AttachmentElementState( * @param formAttachment The [FormAttachment] that this state represents. */ internal class FormAttachmentState( - val name: String, + name: String, val size: Long, val contentType: String, val elementStateId: Int, @@ -222,6 +249,9 @@ internal class FormAttachmentState( private val formAttachment: FormAttachment? = null ) : Loadable { + var name by mutableStateOf(name) + private set + private val _thumbnail: MutableState = mutableStateOf(null) /** @@ -259,6 +289,13 @@ internal class FormAttachmentState( } } + /** + * Updates the attachment with the given [formAttachment]. + */ + fun update(formAttachment: FormAttachment) { + name = formAttachment.name + } + /** * Loads the attachment and its thumbnail. Use [loadWithParentScope] to load the attachment as * a long-running task. This coroutine will get cancelled if the calling composable is removed @@ -291,10 +328,9 @@ internal class FormAttachmentState( } catch (ex: CancellationException) { result = Result.failure(ex) throw ex - } catch (ex : Exception) { + } catch (ex: Exception) { result = Result.failure(ex) - } - finally { + } finally { if (result.isSuccess) { _loadStatus.value = LoadStatus.Loaded } else { @@ -397,3 +433,14 @@ internal fun AttachmentType.getIcon(): ImageVector = when (this) { AttachmentType.Document -> Icons.Outlined.FilePresent AttachmentType.Other -> Icons.Outlined.FileCopy } + +internal fun Map.getNewAttachmentNameForImageType(): String { + val count = this.count { entry -> + entry.value.contentType.isContentTypeImage() + } + return "Image $count.jpg" +} + +internal fun String.isContentTypeImage(): Boolean { + return this.startsWith("image/") +} diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt index c3062d066..f3c5ff75d 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt @@ -32,7 +32,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Add import androidx.compose.material.icons.rounded.Photo @@ -96,7 +95,7 @@ internal fun AttachmentFormElement( description: String, editable: Boolean, stateId: Int, - attachments: List, + attachments: Map, lazyListState: LazyListState, hasCameraPermission: Boolean, modifier: Modifier = Modifier, @@ -131,14 +130,26 @@ internal fun AttachmentFormElement( } @Composable -private fun Carousel(state: LazyListState, attachments: List) { +private fun Carousel(state: LazyListState, attachments: Map) { + var attachmentCount = rememberSaveable { + attachments.count() + } LazyRow( state = state, horizontalArrangement = Arrangement.spacedBy(15.dp), ) { - items(attachments, key = { it.hashCode() }) { - AttachmentTile(it) + attachments.entries.forEach { entry -> + item(key = entry.key) { + AttachmentTile(entry.value) + } + } + } + LaunchedEffect(attachments) { + if (attachmentCount < attachments.count()) { + // Scroll to the first item when a new attachment is added + state.scrollToItem(0) } + attachmentCount = attachments.count() } } @@ -311,17 +322,6 @@ internal fun ImagePicker(onImageSelected: (Uri) -> Unit) { } } -internal fun List.getNewAttachmentNameForContentType( - contentType: String -): String { - val (attachmentType: AttachmentType, ext: String) = when (contentType) { - "image/jpeg" -> Pair(AttachmentType.Image, "jpg") - else -> Pair(AttachmentType.Other, "") - } - val count = this.count { it.type == attachmentType } - return "$attachmentType $count.$ext" -} - private sealed class PickerStyle { data object File : PickerStyle() data object Camera : PickerStyle() @@ -336,15 +336,18 @@ private fun AttachmentFormElementPreview() { description = "Add attachments", editable = true, stateId = 1, - attachments = listOf( - FormAttachmentState( - "Photo 1.jpg", - 2024, - "image/jpeg", + attachments = mapOf( + Pair( 1, - {}, - "", - scope = rememberCoroutineScope() + FormAttachmentState( + "Photo 1.jpg", + 2024, + "image/jpeg", + 1, + {}, + "", + scope = rememberCoroutineScope() + ) ) ), lazyListState = LazyListState(), diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt index 5bcc832ba..8321e19a4 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt @@ -36,7 +36,7 @@ import com.arcgismaps.toolkit.featureforms.internal.components.attachment.Attach import com.arcgismaps.toolkit.featureforms.internal.components.attachment.ImageCapture import com.arcgismaps.toolkit.featureforms.internal.components.attachment.ImagePicker import com.arcgismaps.toolkit.featureforms.internal.components.attachment.RenameAttachmentDialog -import com.arcgismaps.toolkit.featureforms.internal.components.attachment.getNewAttachmentNameForContentType +import com.arcgismaps.toolkit.featureforms.internal.components.attachment.getNewAttachmentNameForImageType import com.arcgismaps.toolkit.featureforms.internal.components.base.FormStateCollection import com.arcgismaps.toolkit.featureforms.internal.components.codedvalue.CodedValueFieldState import com.arcgismaps.toolkit.featureforms.internal.components.codedvalue.ComboBoxDialog @@ -213,9 +213,7 @@ internal fun FeatureFormDialog(states: FormStateCollection) { ImageCapture { uri -> scope.launch { context.readBytes(uri)?.let { data -> - val name = state.attachments.getNewAttachmentNameForContentType( - contentType - ) + val name = state.attachments.getNewAttachmentNameForImageType() state.addAttachment(name, contentType, data) } dialogRequester.dismissDialog() @@ -230,9 +228,7 @@ internal fun FeatureFormDialog(states: FormStateCollection) { ImagePicker { uri -> scope.launch { context.readBytes(uri)?.let { data -> - val name = state.attachments.getNewAttachmentNameForContentType( - contentType - ) + val name = state.attachments.getNewAttachmentNameForImageType() state.addAttachment(name, contentType, data) } dialogRequester.dismissDialog() From 7e1bcc54f0fd5c00e262d9775b35626cc02d0096 Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Mon, 13 May 2024 16:28:32 -0700 Subject: [PATCH 26/32] fix scroll after new attachment is added --- .../attachment/AttachmentElementState.kt | 29 +++++-------------- .../attachment/AttachmentFormElement.kt | 10 ------- 2 files changed, 8 insertions(+), 31 deletions(-) diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt index d8ece8335..0c8d547c5 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt @@ -51,6 +51,7 @@ import com.arcgismaps.toolkit.featureforms.internal.utils.AttachmentsFileProvide import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch @@ -109,8 +110,7 @@ internal class AttachmentElementState( formElement.attachments.forEach { formAttachment -> val state = attachments[formAttachment.hashCode()] if (state == null) { - // if does not exist - // then add it to the list + // if does not exist then create a new state FormAttachmentState( name = formAttachment.name, size = formAttachment.size, @@ -139,25 +139,12 @@ internal class AttachmentElementState( formElement.addAttachment(name, contentType, data) evaluateExpressions() // refresh the list of attachments - attachments = buildMap { - // add the new attachment to the front of the list - val formAttachment = formElement.attachments.last() - FormAttachmentState( - name = formAttachment.name, - size = formAttachment.size, - contentType = formAttachment.contentType, - elementStateId = id, - deleteAttachment = { deleteAttachment(formAttachment) }, - filesDir = filesDir, - scope = scope, - formAttachment = formAttachment - ).also { newState -> - // load the attachment that was just added - newState.loadWithParentScope() - put(formAttachment.hashCode(), newState) - } - putAll(attachments) - } + loadAttachments() + // load the new attachment + attachments.entries.last().value.loadWithParentScope() + // scroll to the new attachment after a delay to allow the recomposition to complete + delay(100) + lazyListState.scrollToItem(attachments.count()) } private suspend fun deleteAttachment(formAttachment: FormAttachment) { diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt index f3c5ff75d..edecbdc77 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt @@ -131,9 +131,6 @@ internal fun AttachmentFormElement( @Composable private fun Carousel(state: LazyListState, attachments: Map) { - var attachmentCount = rememberSaveable { - attachments.count() - } LazyRow( state = state, horizontalArrangement = Arrangement.spacedBy(15.dp), @@ -144,13 +141,6 @@ private fun Carousel(state: LazyListState, attachments: Map Date: Tue, 14 May 2024 16:08:47 -0700 Subject: [PATCH 27/32] refactored api usage --- gradle.properties | 2 +- .../toolkit/featureforms/FeatureForm.kt | 6 +- .../attachment/AttachmentElementState.kt | 116 +++++++++--------- .../components/attachment/AttachmentTile.kt | 23 ++-- 4 files changed, 72 insertions(+), 75 deletions(-) diff --git a/gradle.properties b/gradle.properties index 49d3649bd..aa0e03b3d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -54,4 +54,4 @@ ignoreBuildNumber=false # these versions define the dependency of the ArcGIS Maps SDK for Kotlin dependency # and are generally not overridden at the command line unless a special build is requested. sdkVersionNumber=200.5.0 -sdkBuildNumber=4220 +sdkBuildNumber=4237 diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/FeatureForm.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/FeatureForm.kt index d7da3939b..cb8bed11c 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/FeatureForm.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/FeatureForm.kt @@ -50,7 +50,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp -import com.arcgismaps.mapping.featureforms.AttachmentFormElement +import com.arcgismaps.mapping.featureforms.AttachmentsFormElement import com.arcgismaps.mapping.featureforms.ComboBoxFormInput import com.arcgismaps.mapping.featureforms.DateTimePickerFormInput import com.arcgismaps.mapping.featureforms.FeatureForm @@ -261,7 +261,7 @@ private fun FeatureFormBody( ) } - is AttachmentFormElement -> { + is AttachmentsFormElement -> { AttachmentFormElement( state = entry.getState(), Modifier @@ -352,7 +352,7 @@ internal fun rememberStates( states.add(element, groupState) } - is AttachmentFormElement -> { + is AttachmentsFormElement -> { val state = rememberAttachmentElementState(form, element) states.add(element, state) } diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt index 0c8d547c5..275e615f6 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt @@ -43,9 +43,11 @@ import androidx.compose.ui.platform.LocalContext import androidx.core.content.ContextCompat import com.arcgismaps.LoadStatus import com.arcgismaps.Loadable -import com.arcgismaps.mapping.featureforms.AttachmentFormElement +import com.arcgismaps.mapping.featureforms.AnyAttachmentsFormInput +import com.arcgismaps.mapping.featureforms.AttachmentsFormElement import com.arcgismaps.mapping.featureforms.FeatureForm import com.arcgismaps.mapping.featureforms.FormAttachment +import com.arcgismaps.mapping.featureforms.FormAttachmentType import com.arcgismaps.toolkit.featureforms.internal.components.base.FormElementState import com.arcgismaps.toolkit.featureforms.internal.utils.AttachmentsFileProvider import kotlinx.coroutines.CancellationException @@ -69,7 +71,7 @@ import java.util.Objects */ internal class AttachmentElementState( id: Int, - private val formElement: AttachmentFormElement, + private val formElement: AttachmentsFormElement, private val scope: CoroutineScope, private val evaluateExpressions: suspend () -> Unit, private val filesDir: String @@ -94,20 +96,32 @@ internal class AttachmentElementState( */ val lazyListState = LazyListState() + /** + * The input type of the attachment form element. + */ + val input = formElement.input + init { scope.launch { - loadAttachments() + formElement.fetchAttachments() + formElement.attachments.collect { list -> + buildAttachmentStates(list) + } + } + when (formElement.input) { + is AnyAttachmentsFormInput -> TODO() } } /** - * Loads the attachments associated with the form element. This clears the current list of - * attachments and updates it with the list of attachments from the [formElement]. + * Loads the attachments provided in the [list] and transforms them into state objects + * to produce the [attachments] map. This will generate a new map of attachments but will + * reuse the existing state objects if they exist while updating their properties. */ - private suspend fun loadAttachments() { - formElement.fetchAttachments() + private suspend fun buildAttachmentStates(list: List) { attachments = buildMap { - formElement.attachments.forEach { formAttachment -> + list.forEach { formAttachment -> + // get the current state of the attachment if it exists val state = attachments[formAttachment.hashCode()] if (state == null) { // if does not exist then create a new state @@ -115,6 +129,7 @@ internal class AttachmentElementState( name = formAttachment.name, size = formAttachment.size, contentType = formAttachment.contentType, + type = formAttachment.type, elementStateId = id, deleteAttachment = { deleteAttachment(formAttachment) }, filesDir = filesDir, @@ -122,6 +137,9 @@ internal class AttachmentElementState( formAttachment = formAttachment ).also { newState -> put(formAttachment.hashCode(), newState) + if (formAttachment.loadStatus.value is LoadStatus.Loaded) { + scope.launch { newState.load() } + } } } else { // update the current state @@ -138,8 +156,6 @@ internal class AttachmentElementState( suspend fun addAttachment(name: String, contentType: String, data: ByteArray) { formElement.addAttachment(name, contentType, data) evaluateExpressions() - // refresh the list of attachments - loadAttachments() // load the new attachment attachments.entries.last().value.loadWithParentScope() // scroll to the new attachment after a delay to allow the recomposition to complete @@ -149,14 +165,12 @@ internal class AttachmentElementState( private suspend fun deleteAttachment(formAttachment: FormAttachment) { formElement.deleteAttachment(formAttachment) - attachments = attachments.filter { it.key != formAttachment.hashCode() } } suspend fun renameAttachment(name: String, newName: String) { - val formAttachment = formElement.attachments.firstOrNull { it.name == name } ?: return + val formAttachment = formElement.attachments.value.firstOrNull { it.name == name } ?: return if (formAttachment.name != newName) { formElement.renameAttachment(formAttachment, newName) - loadAttachments() } } @@ -167,7 +181,7 @@ internal class AttachmentElementState( companion object { fun Saver( - attachmentFormElement: AttachmentFormElement, + attachmentFormElement: AttachmentsFormElement, scope: CoroutineScope, evaluateExpressions: suspend () -> Unit, filesDir: String @@ -175,11 +189,11 @@ internal class AttachmentElementState( save = { buildList { // save the keys of attachments that have been loaded - it.attachments.forEach { entry -> - if (entry.value.loadStatus.value is LoadStatus.Loaded) { - add(entry.key) - } - } +// it.attachments.forEach { entry -> +// if (entry.value.loadStatus.value is LoadStatus.Loaded) { +// add(entry.key) +// } +// } // save the index of the first visible item add(it.lazyListState.firstVisibleItemIndex) add(it.lazyListState.firstVisibleItemScrollOffset) @@ -193,20 +207,19 @@ internal class AttachmentElementState( evaluateExpressions = evaluateExpressions, filesDir = filesDir ).also { - scope.launch { - it.loadAttachments() - // load the attachments that were previously loaded - for (i in savedList.dropLast(2)) { - it.attachments[i]?.loadWithParentScope() - } - // scroll to the last visible item - val firstVisibleItemIndex = savedList[savedList.count() - 2] - val firstVisibleItemScrollOffset = savedList[savedList.count() - 1] - it.lazyListState.scrollToItem( - firstVisibleItemIndex, - firstVisibleItemScrollOffset - ) - } +// scope.launch { +// // load the attachments that were previously loaded +// for (i in savedList.dropLast(2)) { +// it.attachments[i]?.loadWithParentScope() +// } +// // scroll to the last visible item +// val firstVisibleItemIndex = savedList[savedList.count() - 2] +// val firstVisibleItemScrollOffset = savedList[savedList.count() - 1] +// it.lazyListState.scrollToItem( +// firstVisibleItemIndex, +// firstVisibleItemScrollOffset +// ) +// } } } ) @@ -219,6 +232,7 @@ internal class AttachmentElementState( * @param name The name of the attachment. * @param size The size of the attachment. * @param contentType The content type of the attachment. + * @param type The type of the attachment. * @param elementStateId The ID of the [AttachmentElementState] that created this attachment. * @param deleteAttachment A function to delete the attachment. * @param filesDir The directory where the attachments are stored. @@ -229,6 +243,7 @@ internal class FormAttachmentState( name: String, val size: Long, val contentType: String, + val type : FormAttachmentType, val elementStateId: Int, val deleteAttachment: suspend () -> Unit, private val filesDir: String, @@ -249,7 +264,7 @@ internal class FormAttachmentState( /** * The type of the attachment. */ - val type: AttachmentType = getAttachmentType(name) + //val type: AttachmentType = getAttachmentType(name) private val _loadStatus: MutableStateFlow = MutableStateFlow(LoadStatus.NotLoaded) override val loadStatus = _loadStatus.asStateFlow() @@ -370,7 +385,7 @@ internal class FormAttachmentState( @Composable internal fun rememberAttachmentElementState( form: FeatureForm, - attachmentFormElement: AttachmentFormElement + attachmentFormElement: AttachmentsFormElement ): AttachmentElementState { val scope = rememberCoroutineScope() val context = LocalContext.current @@ -393,32 +408,13 @@ internal fun rememberAttachmentElementState( } } -internal sealed class AttachmentType { - data object Image : AttachmentType() - data object Audio : AttachmentType() - data object Video : AttachmentType() - data object Document : AttachmentType() - data object Other : AttachmentType() -} - -internal fun getAttachmentType(filename: String): AttachmentType { - val extension = filename.substring(filename.lastIndexOf(".") + 1) - return when (extension) { - "jpg", "jpeg", "png", "gif", "bmp" -> AttachmentType.Image - "mp3", "wav", "ogg", "flac" -> AttachmentType.Audio - "mp4", "avi", "mov", "wmv", "flv" -> AttachmentType.Video - "doc", "docx", "pdf", "txt", "rtf" -> AttachmentType.Document - else -> AttachmentType.Other - } -} - @Composable -internal fun AttachmentType.getIcon(): ImageVector = when (this) { - AttachmentType.Image -> Icons.Outlined.Image - AttachmentType.Audio -> Icons.Outlined.AudioFile - AttachmentType.Video -> Icons.Outlined.VideoCameraBack - AttachmentType.Document -> Icons.Outlined.FilePresent - AttachmentType.Other -> Icons.Outlined.FileCopy +internal fun FormAttachmentType.getIcon(): ImageVector = when (this) { + FormAttachmentType.Image -> Icons.Outlined.Image + FormAttachmentType.Audio -> Icons.Outlined.AudioFile + FormAttachmentType.Video -> Icons.Outlined.VideoCameraBack + FormAttachmentType.Document -> Icons.Outlined.FilePresent + FormAttachmentType.Other -> Icons.Outlined.FileCopy } internal fun Map.getNewAttachmentNameForImageType(): String { diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt index 40d38f05d..d57295bc9 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt @@ -90,6 +90,7 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import com.arcgismaps.LoadStatus +import com.arcgismaps.mapping.featureforms.FormAttachmentType import com.arcgismaps.toolkit.featureforms.R import com.arcgismaps.toolkit.featureforms.internal.utils.AttachmentsFileProvider import com.arcgismaps.toolkit.featureforms.internal.utils.DialogType @@ -127,13 +128,15 @@ internal fun AttachmentTile( Box(modifier = Modifier) { when (loadStatus) { LoadStatus.Loaded -> LoadedView( - thumbnail = thumbnail, - title = state.name + title = state.name, + type = state.type, + thumbnail = thumbnail ) LoadStatus.Loading -> DefaultView( title = state.name, size = state.size, + type = state.type, isLoading = true, isError = false ) @@ -141,6 +144,7 @@ internal fun AttachmentTile( LoadStatus.NotLoaded -> DefaultView( title = state.name, size = state.size, + type = state.type, isLoading = false, isError = false ) @@ -148,6 +152,7 @@ internal fun AttachmentTile( is LoadStatus.FailedToLoad -> DefaultView( title = state.name, size = state.size, + type = state.type, isLoading = false, isError = true ) @@ -239,13 +244,11 @@ internal fun AttachmentTile( @Composable private fun LoadedView( - thumbnail: ImageBitmap?, title: String, + type: FormAttachmentType, + thumbnail: ImageBitmap?, modifier: Modifier = Modifier ) { - val attachmentType = remember(title) { - getAttachmentType(title) - } Box( modifier = modifier .fillMaxSize() @@ -259,7 +262,7 @@ private fun LoadedView( ) } else { Icon( - imageVector = attachmentType.getIcon(), + imageVector = type.getIcon(), contentDescription = null, modifier = Modifier .padding(top = 10.dp, bottom = 25.dp) @@ -294,13 +297,11 @@ private fun LoadedView( private fun DefaultView( title: String, size: Long, + type: FormAttachmentType, isLoading: Boolean, isError: Boolean, modifier: Modifier = Modifier, ) { - val attachmentType = remember(title) { - getAttachmentType(title) - } Column( modifier = modifier .fillMaxSize() @@ -333,7 +334,7 @@ private fun DefaultView( ) } else { Icon( - imageVector = attachmentType.getIcon(), + imageVector = type.getIcon(), contentDescription = null, modifier = Modifier.size(20.dp) ) From ad9c37a9d0a7456295fa353e39e99a05f4dbbc95 Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Wed, 15 May 2024 15:28:05 -0700 Subject: [PATCH 28/32] update build --- gradle.properties | 2 +- .../attachment/AttachmentElementState.kt | 84 +++++++++---------- .../attachment/AttachmentFormElement.kt | 2 + 3 files changed, 44 insertions(+), 44 deletions(-) diff --git a/gradle.properties b/gradle.properties index aa0e03b3d..ddae3fcac 100644 --- a/gradle.properties +++ b/gradle.properties @@ -54,4 +54,4 @@ ignoreBuildNumber=false # these versions define the dependency of the ArcGIS Maps SDK for Kotlin dependency # and are generally not overridden at the command line unless a special build is requested. sdkVersionNumber=200.5.0 -sdkBuildNumber=4237 +sdkBuildNumber=4239 diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt index 275e615f6..649afa2e7 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt @@ -19,6 +19,7 @@ package com.arcgismaps.toolkit.featureforms.internal.components.attachment import android.Manifest import android.content.Context import android.content.pm.PackageManager +import android.util.Log import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.AudioFile @@ -27,7 +28,9 @@ import androidx.compose.material.icons.outlined.FilePresent import androidx.compose.material.icons.outlined.Image import androidx.compose.material.icons.outlined.VideoCameraBack import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.runtime.MutableState +import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -43,7 +46,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.core.content.ContextCompat import com.arcgismaps.LoadStatus import com.arcgismaps.Loadable -import com.arcgismaps.mapping.featureforms.AnyAttachmentsFormInput import com.arcgismaps.mapping.featureforms.AttachmentsFormElement import com.arcgismaps.mapping.featureforms.FeatureForm import com.arcgismaps.mapping.featureforms.FormAttachment @@ -69,6 +71,7 @@ import java.util.Objects * @param scope The coroutine scope used to launch coroutines. * @param evaluateExpressions A method to evaluates the expressions in the form. */ +@Stable internal class AttachmentElementState( id: Int, private val formElement: AttachmentsFormElement, @@ -108,9 +111,6 @@ internal class AttachmentElementState( buildAttachmentStates(list) } } - when (formElement.input) { - is AnyAttachmentsFormInput -> TODO() - } } /** @@ -137,6 +137,9 @@ internal class AttachmentElementState( formAttachment = formAttachment ).also { newState -> put(formAttachment.hashCode(), newState) + // if the attachment is already loaded then re-load the new state + // this is useful during a configuration change when the form attachment + // objects have already been loaded by the state object. if (formAttachment.loadStatus.value is LoadStatus.Loaded) { scope.launch { newState.load() } } @@ -154,13 +157,14 @@ internal class AttachmentElementState( * Adds an attachment with the given [name], [contentType], and [data]. */ suspend fun addAttachment(name: String, contentType: String, data: ByteArray) { - formElement.addAttachment(name, contentType, data) - evaluateExpressions() - // load the new attachment - attachments.entries.last().value.loadWithParentScope() - // scroll to the new attachment after a delay to allow the recomposition to complete - delay(100) - lazyListState.scrollToItem(attachments.count()) + formElement.addAttachment(name, contentType, data).onSuccess { + evaluateExpressions() + // load the new attachment + attachments.entries.lastOrNull()?.value?.loadWithParentScope() + // scroll to the new attachment after a delay to allow the recomposition to complete + delay(100) + lazyListState.scrollToItem(attachments.count()) + } } private suspend fun deleteAttachment(formAttachment: FormAttachment) { @@ -188,12 +192,6 @@ internal class AttachmentElementState( ): Saver = listSaver( save = { buildList { - // save the keys of attachments that have been loaded -// it.attachments.forEach { entry -> -// if (entry.value.loadStatus.value is LoadStatus.Loaded) { -// add(entry.key) -// } -// } // save the index of the first visible item add(it.lazyListState.firstVisibleItemIndex) add(it.lazyListState.firstVisibleItemScrollOffset) @@ -207,19 +205,17 @@ internal class AttachmentElementState( evaluateExpressions = evaluateExpressions, filesDir = filesDir ).also { -// scope.launch { -// // load the attachments that were previously loaded -// for (i in savedList.dropLast(2)) { -// it.attachments[i]?.loadWithParentScope() -// } -// // scroll to the last visible item -// val firstVisibleItemIndex = savedList[savedList.count() - 2] -// val firstVisibleItemScrollOffset = savedList[savedList.count() - 1] -// it.lazyListState.scrollToItem( -// firstVisibleItemIndex, -// firstVisibleItemScrollOffset -// ) -// } + scope.launch { + if (savedList.count() == 2) { + // scroll to the last visible item + val firstVisibleItemIndex = savedList[0] + val firstVisibleItemScrollOffset = savedList[1] + it.lazyListState.scrollToItem( + firstVisibleItemIndex, + firstVisibleItemScrollOffset + ) + } + } } } ) @@ -239,11 +235,12 @@ internal class AttachmentElementState( * @param scope The coroutine scope used to launch coroutines. * @param formAttachment The [FormAttachment] that this state represents. */ +@Stable internal class FormAttachmentState( name: String, val size: Long, val contentType: String, - val type : FormAttachmentType, + val type: FormAttachmentType, val elementStateId: Int, val deleteAttachment: suspend () -> Unit, private val filesDir: String, @@ -261,11 +258,6 @@ internal class FormAttachmentState( */ val thumbnail: State = _thumbnail - /** - * The type of the attachment. - */ - //val type: AttachmentType = getAttachmentType(name) - private val _loadStatus: MutableStateFlow = MutableStateFlow(LoadStatus.NotLoaded) override val loadStatus = _loadStatus.asStateFlow() @@ -280,6 +272,11 @@ internal class FormAttachmentState( */ private val attachmentsDir = "feature_forms_attachments" + /** + * The size of the thumbnail image. + */ + private val thumbnailSize = Pair(368, 300) + /** * Loads the attachment and its thumbnail in the coroutine scope of the state object that * created this attachment. Usually, this is the [AttachmentElementState] that created this @@ -292,9 +289,10 @@ internal class FormAttachmentState( } /** - * Updates the attachment with the given [formAttachment]. + * Updates the attachment properties with the given [formAttachment]. */ fun update(formAttachment: FormAttachment) { + // only name is updated since renameAttachment() is the only update call that can be made name = formAttachment.name } @@ -315,9 +313,10 @@ internal class FormAttachmentState( }.onSuccess { val data = formAttachment.attachment?.fetchData()?.getOrNull() if (data != null) { - formAttachment.createFullImage().onSuccess { - _thumbnail.value = it.bitmap.asImageBitmap() - } + formAttachment.createThumbnail(thumbnailSize.first, thumbnailSize.second) + .onSuccess { + _thumbnail.value = it.bitmap.asImageBitmap() + } // write the data to disk only if the file does not exist if (!File(filePath).exists()) { writeDataToDisk(data) @@ -344,12 +343,11 @@ internal class FormAttachmentState( } override fun cancelLoad() { - formAttachment?.cancelLoad() + // cancel op not supported } override suspend fun retryLoad(): Result { - return formAttachment?.retryLoad() - ?: return Result.failure(Exception("Form attachment is null")) + return load() } override fun hashCode(): Int { diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt index edecbdc77..83a9c888a 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt @@ -62,6 +62,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp +import com.arcgismaps.mapping.featureforms.FormAttachmentType import com.arcgismaps.toolkit.featureforms.R import com.arcgismaps.toolkit.featureforms.internal.utils.AttachmentsFileProvider import com.arcgismaps.toolkit.featureforms.internal.utils.DialogType @@ -333,6 +334,7 @@ private fun AttachmentFormElementPreview() { "Photo 1.jpg", 2024, "image/jpeg", + FormAttachmentType.Image, 1, {}, "", From 98987aba95706cf3acc6f429ac1cca073af19020 Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Fri, 17 May 2024 11:06:10 -0700 Subject: [PATCH 29/32] changed the state holder from map to list --- gradle.properties | 2 +- .../attachment/AttachmentElementState.kt | 103 ++++++++++-------- .../attachment/AttachmentFormElement.kt | 34 +++--- .../components/attachment/AttachmentTile.kt | 13 ++- .../featureforms/internal/utils/Dialog.kt | 22 +--- 5 files changed, 86 insertions(+), 88 deletions(-) diff --git a/gradle.properties b/gradle.properties index ddae3fcac..6ba92e6ad 100644 --- a/gradle.properties +++ b/gradle.properties @@ -54,4 +54,4 @@ ignoreBuildNumber=false # these versions define the dependency of the ArcGIS Maps SDK for Kotlin dependency # and are generally not overridden at the command line unless a special build is requested. sdkVersionNumber=200.5.0 -sdkBuildNumber=4239 +sdkBuildNumber=4240 diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt index 649afa2e7..b18718425 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt @@ -19,7 +19,6 @@ package com.arcgismaps.toolkit.featureforms.internal.components.attachment import android.Manifest import android.content.Context import android.content.pm.PackageManager -import android.util.Log import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.AudioFile @@ -28,11 +27,11 @@ import androidx.compose.material.icons.outlined.FilePresent import androidx.compose.material.icons.outlined.Image import androidx.compose.material.icons.outlined.VideoCameraBack import androidx.compose.runtime.Composable -import androidx.compose.runtime.Immutable import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.Saver @@ -87,7 +86,7 @@ internal class AttachmentElementState( /** * The attachments associated with the form element. */ - var attachments by mutableStateOf(emptyMap()) + var attachments = mutableStateListOf() /** * Indicates whether the attachment form element is editable. @@ -106,9 +105,8 @@ internal class AttachmentElementState( init { scope.launch { - formElement.fetchAttachments() - formElement.attachments.collect { list -> - buildAttachmentStates(list) + formElement.fetchAttachments().onSuccess { + buildAttachmentStates(formElement.attachments.value) } } } @@ -119,37 +117,27 @@ internal class AttachmentElementState( * reuse the existing state objects if they exist while updating their properties. */ private suspend fun buildAttachmentStates(list: List) { - attachments = buildMap { - list.forEach { formAttachment -> - // get the current state of the attachment if it exists - val state = attachments[formAttachment.hashCode()] - if (state == null) { - // if does not exist then create a new state - FormAttachmentState( - name = formAttachment.name, - size = formAttachment.size, - contentType = formAttachment.contentType, - type = formAttachment.type, - elementStateId = id, - deleteAttachment = { deleteAttachment(formAttachment) }, - filesDir = filesDir, - scope = scope, - formAttachment = formAttachment - ).also { newState -> - put(formAttachment.hashCode(), newState) - // if the attachment is already loaded then re-load the new state - // this is useful during a configuration change when the form attachment - // objects have already been loaded by the state object. - if (formAttachment.loadStatus.value is LoadStatus.Loaded) { - scope.launch { newState.load() } - } - } - } else { - // update the current state - state.update(formAttachment) - put(formAttachment.hashCode(), state) - } + attachments.clear() + list.forEach { formAttachment -> + // create a new state + val state = FormAttachmentState( + name = formAttachment.name, + size = formAttachment.size, + contentType = formAttachment.contentType, + type = formAttachment.type, + elementStateId = id, + deleteAttachment = { deleteAttachment(formAttachment) }, + filesDir = filesDir, + scope = scope, + formAttachment = formAttachment + ) + // if the attachment is already loaded then re-load the new state + // this is useful during a configuration change when the form attachment + // objects have already been loaded by the state object. + if (formAttachment.loadStatus.value is LoadStatus.Loaded) { + state.loadWithParentScope() } + attachments.add(state) } } @@ -157,10 +145,24 @@ internal class AttachmentElementState( * Adds an attachment with the given [name], [contentType], and [data]. */ suspend fun addAttachment(name: String, contentType: String, data: ByteArray) { - formElement.addAttachment(name, contentType, data).onSuccess { - evaluateExpressions() + formElement.addAttachment(name, contentType, data).onSuccess { formAttachment -> + // create a new state + val state = FormAttachmentState( + name = formAttachment.name, + size = formAttachment.size, + contentType = formAttachment.contentType, + type = formAttachment.type, + elementStateId = id, + deleteAttachment = { deleteAttachment(formAttachment) }, + filesDir = filesDir, + scope = scope, + formAttachment = formAttachment + ) + attachments.add(state) // load the new attachment - attachments.entries.lastOrNull()?.value?.loadWithParentScope() + state.loadWithParentScope() + // evaluate expressions after adding the attachment + evaluateExpressions() // scroll to the new attachment after a delay to allow the recomposition to complete delay(100) lazyListState.scrollToItem(attachments.count()) @@ -168,13 +170,22 @@ internal class AttachmentElementState( } private suspend fun deleteAttachment(formAttachment: FormAttachment) { - formElement.deleteAttachment(formAttachment) + formElement.deleteAttachment(formAttachment).onSuccess { + attachments.removeIf { + it.formAttachment == formAttachment + } + evaluateExpressions() + } } - suspend fun renameAttachment(name: String, newName: String) { - val formAttachment = formElement.attachments.value.firstOrNull { it.name == name } ?: return + suspend fun renameAttachment(formAttachment: FormAttachment, newName: String) { if (formAttachment.name != newName) { - formElement.renameAttachment(formAttachment, newName) + formElement.renameAttachment(formAttachment, newName).onSuccess { + attachments.firstOrNull { + it.formAttachment == formAttachment + }?.update(formAttachment) + evaluateExpressions() + } } } @@ -245,7 +256,7 @@ internal class FormAttachmentState( val deleteAttachment: suspend () -> Unit, private val filesDir: String, private val scope: CoroutineScope, - private val formAttachment: FormAttachment? = null + val formAttachment: FormAttachment? = null ) : Loadable { var name by mutableStateOf(name) @@ -415,9 +426,9 @@ internal fun FormAttachmentType.getIcon(): ImageVector = when (this) { FormAttachmentType.Other -> Icons.Outlined.FileCopy } -internal fun Map.getNewAttachmentNameForImageType(): String { +internal fun List.getNewAttachmentNameForImageType(): String { val count = this.count { entry -> - entry.value.contentType.isContentTypeImage() + entry.contentType.isContentTypeImage() } return "Image $count.jpg" } diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt index 83a9c888a..3d2be8612 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentFormElement.kt @@ -32,6 +32,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Add import androidx.compose.material.icons.rounded.Photo @@ -96,7 +97,7 @@ internal fun AttachmentFormElement( description: String, editable: Boolean, stateId: Int, - attachments: Map, + attachments: List, lazyListState: LazyListState, hasCameraPermission: Boolean, modifier: Modifier = Modifier, @@ -131,15 +132,15 @@ internal fun AttachmentFormElement( } @Composable -private fun Carousel(state: LazyListState, attachments: Map) { +private fun Carousel(state: LazyListState, attachments: List) { LazyRow( state = state, horizontalArrangement = Arrangement.spacedBy(15.dp), ) { - attachments.entries.forEach { entry -> - item(key = entry.key) { - AttachmentTile(entry.value) - } + items(attachments, key = { + it.formAttachment.hashCode() + }) { attachment -> + AttachmentTile(attachment) } } } @@ -327,19 +328,16 @@ private fun AttachmentFormElementPreview() { description = "Add attachments", editable = true, stateId = 1, - attachments = mapOf( - Pair( + attachments = listOf( + FormAttachmentState( + "Photo 1.jpg", + 2024, + "image/jpeg", + FormAttachmentType.Image, 1, - FormAttachmentState( - "Photo 1.jpg", - 2024, - "image/jpeg", - FormAttachmentType.Image, - 1, - {}, - "", - scope = rememberCoroutineScope() - ) + {}, + "", + scope = rememberCoroutineScope() ) ), lazyListState = LazyListState(), diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt index d57295bc9..061a54cf2 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentTile.kt @@ -171,12 +171,15 @@ internal fun AttachmentTile( }, onClick = { showContextMenu = false - dialogRequester.requestDialog( - DialogType.RenameAttachmentDialog( - stateId = state.elementStateId, - name = state.name, + state.formAttachment?.let { + dialogRequester.requestDialog( + DialogType.RenameAttachmentDialog( + stateId = state.elementStateId, + formAttachment = state.formAttachment, + name = state.name, + ) ) - ) + } }) DropdownMenuItem( text = { Text(text = stringResource(R.string.delete)) }, diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt index 22b9a99a6..374db579d 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/utils/Dialog.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.window.core.layout.WindowSizeClass import androidx.window.layout.WindowMetricsCalculator +import com.arcgismaps.mapping.featureforms.FormAttachment import com.arcgismaps.toolkit.featureforms.R import com.arcgismaps.toolkit.featureforms.internal.components.attachment.AttachmentElementState import com.arcgismaps.toolkit.featureforms.internal.components.attachment.ImageCapture @@ -133,6 +134,7 @@ internal sealed class DialogType { */ data class RenameAttachmentDialog( val stateId: Int, + val formAttachment : FormAttachment, val name: String, ) : DialogType() } @@ -255,6 +257,7 @@ internal fun FeatureFormDialog(states: FormStateCollection) { is DialogType.RenameAttachmentDialog -> { val stateId = (dialogType as DialogType.RenameAttachmentDialog).stateId val name = (dialogType as DialogType.RenameAttachmentDialog).name + val formAttachment = (dialogType as DialogType.RenameAttachmentDialog).formAttachment val state = states[stateId] as? AttachmentElementState if (state == null) { dialogRequester.dismissDialog() @@ -264,7 +267,7 @@ internal fun FeatureFormDialog(states: FormStateCollection) { name = name, onRename = { newName -> scope.launch { - state.renameAttachment(name, newName) + state.renameAttachment(formAttachment, newName) } dialogRequester.dismissDialog() } @@ -273,23 +276,6 @@ internal fun FeatureFormDialog(states: FormStateCollection) { } } - is DialogType.RenameAttachmentDialog -> { - val stateId = (dialogType as DialogType.RenameAttachmentDialog).stateId - val name = (dialogType as DialogType.RenameAttachmentDialog).name - val state = states[stateId]!! as AttachmentElementState - RenameAttachmentDialog( - name = name, - onRename = { newName -> - scope.launch { - state.renameAttachment(name, newName) - dialogRequester.dismissDialog() - } - } - ) { - dialogRequester.dismissDialog() - } - } - else -> { // clear focus from the originating tapped field if (dialogType == null) { From f92645161827da239c8083a667c998563c8349c9 Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Mon, 20 May 2024 15:08:34 -0700 Subject: [PATCH 30/32] use event type --- gradle.properties | 2 +- .../attachment/AttachmentElementState.kt | 39 ++++++++++++------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/gradle.properties b/gradle.properties index 6ba92e6ad..f4eaae5d2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -54,4 +54,4 @@ ignoreBuildNumber=false # these versions define the dependency of the ArcGIS Maps SDK for Kotlin dependency # and are generally not overridden at the command line unless a special build is requested. sdkVersionNumber=200.5.0 -sdkBuildNumber=4240 +sdkBuildNumber=4244 diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt index b18718425..8bc9236b4 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt @@ -45,6 +45,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.core.content.ContextCompat import com.arcgismaps.LoadStatus import com.arcgismaps.Loadable +import com.arcgismaps.mapping.featureforms.AttachmentChangeType import com.arcgismaps.mapping.featureforms.AttachmentsFormElement import com.arcgismaps.mapping.featureforms.FeatureForm import com.arcgismaps.mapping.featureforms.FormAttachment @@ -106,7 +107,27 @@ internal class AttachmentElementState( init { scope.launch { formElement.fetchAttachments().onSuccess { - buildAttachmentStates(formElement.attachments.value) + buildAttachmentStates(formElement.attachments) + } + } + scope.launch { + formElement.attachmentChanged.collect { + when (it.changeType) { + is AttachmentChangeType.Deletion -> { + attachments.removeIf { state -> + state.formAttachment == it.attachment + } + } + + is AttachmentChangeType.Rename -> { + attachments.firstOrNull { state -> + state.formAttachment == it.attachment + }?.update(it.attachment) + } + + else -> {} + } + evaluateExpressions() } } } @@ -161,8 +182,6 @@ internal class AttachmentElementState( attachments.add(state) // load the new attachment state.loadWithParentScope() - // evaluate expressions after adding the attachment - evaluateExpressions() // scroll to the new attachment after a delay to allow the recomposition to complete delay(100) lazyListState.scrollToItem(attachments.count()) @@ -170,22 +189,12 @@ internal class AttachmentElementState( } private suspend fun deleteAttachment(formAttachment: FormAttachment) { - formElement.deleteAttachment(formAttachment).onSuccess { - attachments.removeIf { - it.formAttachment == formAttachment - } - evaluateExpressions() - } + formElement.deleteAttachment(formAttachment) } suspend fun renameAttachment(formAttachment: FormAttachment, newName: String) { if (formAttachment.name != newName) { - formElement.renameAttachment(formAttachment, newName).onSuccess { - attachments.firstOrNull { - it.formAttachment == formAttachment - }?.update(formAttachment) - evaluateExpressions() - } + formElement.renameAttachment(formAttachment, newName) } } From 462ecf85f64bf492646e34aafccc5f0cb949b80f Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Mon, 20 May 2024 15:21:26 -0700 Subject: [PATCH 31/32] add doc --- .../attachment/AttachmentElementState.kt | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt index 8bc9236b4..92921011f 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt @@ -107,6 +107,7 @@ internal class AttachmentElementState( init { scope.launch { formElement.fetchAttachments().onSuccess { + // build a state list of attachments buildAttachmentStates(formElement.attachments) } } @@ -114,12 +115,14 @@ internal class AttachmentElementState( formElement.attachmentChanged.collect { when (it.changeType) { is AttachmentChangeType.Deletion -> { + // delete the state object attachments.removeIf { state -> state.formAttachment == it.attachment } } is AttachmentChangeType.Rename -> { + // update the state object attachments.firstOrNull { state -> state.formAttachment == it.attachment }?.update(it.attachment) @@ -134,8 +137,7 @@ internal class AttachmentElementState( /** * Loads the attachments provided in the [list] and transforms them into state objects - * to produce the [attachments] map. This will generate a new map of attachments but will - * reuse the existing state objects if they exist while updating their properties. + * to produce the [attachments] list. */ private suspend fun buildAttachmentStates(list: List) { attachments.clear() @@ -188,16 +190,25 @@ internal class AttachmentElementState( } } + /** + * Deletes the given [formAttachment]. + */ private suspend fun deleteAttachment(formAttachment: FormAttachment) { formElement.deleteAttachment(formAttachment) } + /** + * Renames the given [formAttachment] with the new [newName]. + */ suspend fun renameAttachment(formAttachment: FormAttachment, newName: String) { if (formAttachment.name != newName) { formElement.renameAttachment(formAttachment, newName) } } + /** + * Checks if the camera permissions are granted. + */ fun hasCameraPermissions(context: Context): Boolean = ContextCompat.checkSelfPermission( context, Manifest.permission.CAMERA From 36cff1a9bb2efe48671e81e9bad9bb21b7684311 Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Wed, 22 May 2024 15:29:37 -0700 Subject: [PATCH 32/32] react to attachment add event --- .../attachment/AttachmentElementState.kt | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt index 92921011f..313485a97 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/components/attachment/AttachmentElementState.kt @@ -128,7 +128,27 @@ internal class AttachmentElementState( }?.update(it.attachment) } - else -> {} + is AttachmentChangeType.Addition -> { + val formAttachment = it.attachment + // create a new state + val state = FormAttachmentState( + name = formAttachment.name, + size = formAttachment.size, + contentType = formAttachment.contentType, + type = formAttachment.type, + elementStateId = id, + deleteAttachment = { deleteAttachment(formAttachment) }, + filesDir = filesDir, + scope = scope, + formAttachment = formAttachment + ) + attachments.add(state) + // load the new attachment + state.loadWithParentScope() + // scroll to the new attachment after a delay to allow the recomposition to complete + delay(100) + lazyListState.scrollToItem(attachments.count()) + } } evaluateExpressions() } @@ -168,26 +188,7 @@ internal class AttachmentElementState( * Adds an attachment with the given [name], [contentType], and [data]. */ suspend fun addAttachment(name: String, contentType: String, data: ByteArray) { - formElement.addAttachment(name, contentType, data).onSuccess { formAttachment -> - // create a new state - val state = FormAttachmentState( - name = formAttachment.name, - size = formAttachment.size, - contentType = formAttachment.contentType, - type = formAttachment.type, - elementStateId = id, - deleteAttachment = { deleteAttachment(formAttachment) }, - filesDir = filesDir, - scope = scope, - formAttachment = formAttachment - ) - attachments.add(state) - // load the new attachment - state.loadWithParentScope() - // scroll to the new attachment after a delay to allow the recomposition to complete - delay(100) - lazyListState.scrollToItem(attachments.count()) - } + formElement.addAttachment(name, contentType, data) } /**