Skip to content

Forms : View Attachments #422

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 30 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0fb1fba
make formelements observable
kaushikrw Apr 27, 2024
de2abfc
add attachment loading
kaushikrw Apr 29, 2024
ed60c35
revert un-needed changes
kaushikrw Apr 29, 2024
3f526a2
remove saver
kaushikrw Apr 29, 2024
880b8dd
Update AttachmentTile.kt
kaushikrw Apr 29, 2024
e8bfb1f
init add attachments work
kaushikrw Apr 30, 2024
9b6d8b2
added permissions for camera access
kaushikrw May 1, 2024
66a636e
add proper commits for attachments
kaushikrw May 2, 2024
8f3019b
added doc
kaushikrw May 2, 2024
98d9e43
Merge branch 'feature-branches/forms' into kaushik/forms/add-attachments
kaushikrw May 3, 2024
0893571
add editability of attachmentformelement
kaushikrw May 2, 2024
950923c
update doc
kaushikrw May 4, 2024
f4d169c
extract string resource
kaushikrw May 4, 2024
70bb4ef
add delete and rename attachments
kaushikrw May 6, 2024
9012d07
Merge branch 'feature-branches/forms' into kaushik/forms/delete-renam…
kaushikrw May 6, 2024
a13e6ed
initial fix
kaushikrw May 7, 2024
18fab1d
added doc
kaushikrw May 7, 2024
81a8253
fix imports
kaushikrw May 7, 2024
d6b865f
Update AttachmentTile.kt
kaushikrw May 7, 2024
df1abcc
Merge branch 'kaushik/forms/fix-state-ref' into kaushik/forms/delete-…
kaushikrw May 7, 2024
963bf0d
fixed rename dialog
kaushikrw May 7, 2024
598ca1d
Merge branch 'feature-branches/forms' into kaushik/forms/delete-renam…
kaushikrw May 7, 2024
0d679b2
fix conficts
kaushikrw May 8, 2024
d981e45
init working
kaushikrw May 8, 2024
88ca861
update authority
kaushikrw May 8, 2024
5642cde
fix capture uri and delete workaround
kaushikrw May 10, 2024
13ad099
make formattachmentstate loadable
kaushikrw May 10, 2024
f3d2fdd
Update AttachmentElementState.kt
kaushikrw May 10, 2024
5c043dd
Merge branch 'feature-branches/forms' into kaushik/forms/view-attachm…
kaushikrw May 13, 2024
8ac8fcf
guard against index out of bounds
kaushikrw May 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions toolkit/featureforms/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@

<application>
<provider
android:name="com.arcgismaps.toolkit.featureforms.internal.utils.AttachmentCaptureFileProvider"
android:authorities="com.arcgismaps.toolkit.featureforms.capturefileprovider"
android:name="com.arcgismaps.toolkit.featureforms.internal.utils.AttachmentsFileProvider"
android:authorities="${applicationId}.com.arcgismaps.toolkit.featureforms.attachmentsfileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/feature_forms_captured_attachments" />
android:resource="@xml/feature_forms_attachments" />
</provider>
</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -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.graphics.drawable.BitmapDrawable
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.AudioFile
Expand All @@ -39,15 +38,24 @@ 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.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.flow.StateFlow
import kotlinx.coroutines.Dispatchers
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

/**
Expand All @@ -61,7 +69,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,
Expand Down Expand Up @@ -98,7 +107,16 @@ internal class AttachmentElementState(
attachments.clear()
attachments.addAll(
formElement.attachments.map {
FormAttachmentState(this, it, scope)
FormAttachmentState(
name = it.name,
size = it.size,
contentType = it.contentType,
elementStateId = id,
deleteAttachment = { deleteAttachment(it) },
filesDir = filesDir,
scope = scope,
formAttachment = it
)
}
)
}
Expand All @@ -112,14 +130,15 @@ internal class AttachmentElementState(
// refresh the list of attachments
loadAttachments()
// load the attachment that was just added
attachments.last().load()
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)
loadAttachments()
val state = attachments.find { it.name == formAttachment.name } ?: return
attachments.remove(state)
}

suspend fun renameAttachment(name: String, newName: String) {
Expand All @@ -139,7 +158,8 @@ internal class AttachmentElementState(
fun Saver(
attachmentFormElement: AttachmentFormElement,
scope: CoroutineScope,
evaluateExpressions: suspend () -> Unit
evaluateExpressions: suspend () -> Unit,
filesDir: String
): Saver<AttachmentElementState, Any> = listSaver(
save = {
// save the list of indices of attachments that have been loaded
Expand All @@ -149,21 +169,29 @@ internal class AttachmentElementState(
add(i)
}
}
// save the index of the first visible item
add(it.lazyListState.firstVisibleItemIndex)
add(it.lazyListState.firstVisibleItemScrollOffset)
}
},
restore = { savedList ->
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()
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)
}
}
}
Expand All @@ -176,21 +204,23 @@ 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 elementStateId: Int,
val loadStatus: StateFlow<LoadStatus>,
private val onLoadAttachment: suspend () -> Result<Unit>,
private val onLoadThumbnail: suspend () -> Result<BitmapDrawable?>,
val deleteAttachment: suspend () -> Unit,
private val filesDir: String,
private val scope: CoroutineScope,
) {
private val formAttachment: FormAttachment? = null
) : Loadable {
private val _thumbnail: MutableState<ImageBitmap?> = mutableStateOf(null)

/**
Expand All @@ -203,36 +233,96 @@ internal class FormAttachmentState(
*/
val type: AttachmentType = getAttachmentType(name)

constructor(
element: AttachmentElementState,
attachment: FormAttachment,
scope: CoroutineScope
) : this(
name = attachment.name,
size = attachment.size,
elementStateId = element.id,
loadStatus = attachment.loadStatus,
onLoadAttachment = attachment::retryLoad,
onLoadThumbnail = attachment::createFullImage,
deleteAttachment = {
element.deleteAttachment(attachment)
},
scope = scope
)
private val _loadStatus: MutableStateFlow<LoadStatus> = MutableStateFlow(LoadStatus.NotLoaded)
override val loadStatus = _loadStatus.asStateFlow()

/**
* Loads the attachment and its thumbnail.
* The file path of the attachment on disk. This is empty until [load] is called.
*/
fun load() {
var filePath: String = ""
private set

/**
* The directory where the attachments are stored as defined in the [AttachmentsFileProvider].
*/
private val attachmentsDir = "feature_forms_attachments"

/**
* 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 {
onLoadAttachment().onSuccess {
onLoadThumbnail().onSuccess {
if (it != null) {
_thumbnail.value = it.bitmap.asImageBitmap()
load()
}
}

/**
* 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.
*/
override suspend fun load(): Result<Unit> {
_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"))
}
}
}
} catch (ex: CancellationException) {
result = Result.failure(ex)
throw ex
} catch (ex : Exception) {
result = Result.failure(ex)
}
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
}

override fun cancelLoad() {
formAttachment?.cancelLoad()
}

override suspend fun retryLoad(): Result<Unit> {
return formAttachment?.retryLoad()
?: return Result.failure(Exception("Form attachment is null"))
}

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
}

override fun hashCode(): Int {
Expand All @@ -259,19 +349,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
)
}
}
Expand Down
Loading