From 1e8c43d0b01debf59e33b1ac50959fe61cc23ca0 Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Tue, 6 Feb 2024 16:43:15 -0800 Subject: [PATCH 1/4] added mock api --- .../featureforms/api/AttachmentFormElement.kt | 258 ++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/api/AttachmentFormElement.kt diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/api/AttachmentFormElement.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/api/AttachmentFormElement.kt new file mode 100644 index 000000000..9424be606 --- /dev/null +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/api/AttachmentFormElement.kt @@ -0,0 +1,258 @@ +/* + * 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.api + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.drawable.BitmapDrawable +import android.media.ThumbnailUtils +import com.arcgismaps.LoadStatus +import com.arcgismaps.Loadable +import com.arcgismaps.data.ArcGISFeature +import com.arcgismaps.data.Attachment +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.nio.ByteBuffer +import android.content.Context + +/** + * A FormElement type representing an Attachment. Use the factory method [createOrNull] to create + * an instance. + */ +internal class AttachmentFormElement private constructor( + private val feature: ArcGISFeature, + private val filesDir: String +) { + /** + * The input user interface to use for the element. + */ + val input: AttachmentFormInput = AttachmentFormInput.AnyAttachmentFormInput + + /** + * True if the element is editable. False if the element is not editable. + */ + val isEditable: Boolean = feature.canEditAttachments + + private val _attachments: MutableList = mutableListOf() + + /** + * Returns all the current attachments. + */ + val attachments: List = _attachments + + private val mutex = Mutex() + + /** + * Adds the specified [data] as an attachment with the specified [name] and content [contentType]. + * This method is thread-safe. + */ + suspend fun addAttachment( + name: String, + contentType: String, + data: ByteArray + ): Result { + mutex.withLock { + val attachment = feature.addAttachment(name, contentType, data).getOrNull() + return if (attachment != null) { + val formAttachment = FormAttachment(attachment, filesDir) + _attachments.add(formAttachment) + Result.success(formAttachment) + } else { + Result.failure(Exception("Unable to create attachment")) + } + } + } + + /** + * Loads and adds the specified [uri] as a [File] as an attachment with the specified [name]. + */ + suspend fun addAttachment( + name: String, + contentType: String, + uri: String + ): Result { + try { + var bytes: ByteArray? + FileInputStream(File((uri))).use { + bytes = it.readBytes() + } + bytes?.let { + return addAttachment(name, contentType, it) + } + return Result.failure(Exception("Unable to read from $uri")) + } catch (ex: Exception) { + if (ex is CancellationException) { + throw ex + } + return Result.failure(ex) + } + } + + /** + * Adds the specified [bitmapDrawable] as an attachment with the specified [name]. + */ + suspend fun addAttachment( + name: String, + contentType: String, + bitmapDrawable: BitmapDrawable + ): Result { + val byteArray = bitmapDrawable.bitmap.toByteArray() + return addAttachment(name, contentType, byteArray) + } + + /** + * Deletes the specified [attachment]. This method is thread-safe. + */ + suspend fun deleteAttachment(attachment: FormAttachment): Result { + mutex.withLock { + if (_attachments.contains(attachment)) { + feature.deleteAttachment(attachment.attachment).onFailure { + return Result.failure(it) + } + _attachments.remove(attachment) + return Result.success(Unit) + } else { + return Result.failure(NoSuchElementException()) + } + } + } + + companion object { + + /** + * Creates a new [AttachmentFormElement] from the give [feature]. + * + * @param feature The [ArcGISFeature] to create the [AttachmentFormElement] from. + * @param filesDir The directory to the cache any attachments. Use [Context.getFilesDir]. + * + * @return Returns null if unable to create a [AttachmentFormElement]. + */ + suspend fun createOrNull(feature: ArcGISFeature, filesDir: String): AttachmentFormElement? { + feature.load().onFailure { return null } + val featureAttachments = feature.fetchAttachments().onFailure { + return null + }.getOrNull()!! + val formAttachments = featureAttachments.map { + FormAttachment(it, filesDir) + } + return AttachmentFormElement(feature, filesDir).apply { + _attachments.addAll(formAttachments) + } + } + } +} + +/** + * Represents an attachment belonging to a feature form. Wraps the Attachment object and adds additional + * properties and methods to support displaying attachments in a feature form. + * + * The [FormAttachment] must be loaded before calling [createFullImage] or [createThumbnail]. + */ +internal class FormAttachment( + val attachment: Attachment, + private val filesDir: String +) : Loadable { + val contentType: String = attachment.contentType + + var filePath: String = "" + private set + + var isLocal: Boolean = false + private set + + val name: String = attachment.name + + val size: Int = attachment.size + + private val _loadStatus: MutableStateFlow = MutableStateFlow(LoadStatus.NotLoaded) + override val loadStatus: StateFlow = _loadStatus.asStateFlow() + + @Suppress("DEPRECATION") + suspend fun createFullImage(): Result = withContext(Dispatchers.IO) { + return@withContext if (filePath.isNotEmpty()) { + val bitmap = BitmapFactory.decodeFile(filePath) + Result.success(BitmapDrawable(bitmap)) + } else { + Result.failure(Exception("Attachment is not loaded")) + } + } + + @Suppress("DEPRECATION") + suspend fun createThumbnail(width: Int, height: Int): Result = + withContext(Dispatchers.IO) { + return@withContext if (filePath.isNotEmpty()) { + val bitmap = BitmapFactory.decodeFile(filePath) + val thumbnail = ThumbnailUtils.extractThumbnail(bitmap, width, height) + Result.success(BitmapDrawable(thumbnail)) + } else { + Result.failure(Exception("Attachment is not loaded")) + } + } + + override fun cancelLoad() { + /** does nothing in this mock api **/ + } + + override suspend fun load(): Result { + _loadStatus.value = LoadStatus.Loading + try { + if (!attachment.hasFetchedData || filePath.isEmpty()) { + val data = attachment.fetchData().getOrThrow() + val dir = File("$filesDir/attachments") + dir.mkdirs() + val file = File(dir, attachment.name) + file.createNewFile() + FileOutputStream(file).use { + it.write(data) + } + filePath = file.absolutePath + isLocal = true + } + } catch (ex: Exception) { + _loadStatus.value = LoadStatus.FailedToLoad(ex) + if (ex is CancellationException) { + throw ex + } + return Result.failure(ex) + } + _loadStatus.value = LoadStatus.Loaded + return Result.success(Unit) + } + + override suspend fun retryLoad(): Result { + return load() + } +} + +internal sealed class AttachmentFormInput { + data object AnyAttachmentFormInput : AttachmentFormInput() +} + +internal fun Bitmap.toByteArray(): ByteArray { + val byteBuffer = ByteBuffer.allocate(allocationByteCount) + copyPixelsToBuffer(byteBuffer) + return byteBuffer.array() +} From 5c25b6a1421b5b887737fc577e427772ba0dd164 Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Tue, 6 Feb 2024 16:46:02 -0800 Subject: [PATCH 2/4] Update AttachmentFormElement.kt --- .../arcgismaps/toolkit/featureforms/api/AttachmentFormElement.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/api/AttachmentFormElement.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/api/AttachmentFormElement.kt index 9424be606..ff6fba262 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/api/AttachmentFormElement.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/api/AttachmentFormElement.kt @@ -78,6 +78,7 @@ internal class AttachmentFormElement private constructor( val attachment = feature.addAttachment(name, contentType, data).getOrNull() return if (attachment != null) { val formAttachment = FormAttachment(attachment, filesDir) + formAttachment.load() _attachments.add(formAttachment) Result.success(formAttachment) } else { From c2e04fa03db1b418f46afba3ebf2541d42d28dc2 Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Tue, 6 Feb 2024 16:48:53 -0800 Subject: [PATCH 3/4] added check for creating bitmaps --- .../featureforms/api/AttachmentFormElement.kt | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/api/AttachmentFormElement.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/api/AttachmentFormElement.kt index ff6fba262..b71f358f3 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/api/AttachmentFormElement.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/api/AttachmentFormElement.kt @@ -195,7 +195,11 @@ internal class FormAttachment( suspend fun createFullImage(): Result = withContext(Dispatchers.IO) { return@withContext if (filePath.isNotEmpty()) { val bitmap = BitmapFactory.decodeFile(filePath) - Result.success(BitmapDrawable(bitmap)) + if (bitmap != null) { + Result.success(BitmapDrawable(bitmap)) + } else { + Result.failure(Exception("Unable to create an image")) + } } else { Result.failure(Exception("Attachment is not loaded")) } @@ -206,8 +210,12 @@ internal class FormAttachment( withContext(Dispatchers.IO) { return@withContext if (filePath.isNotEmpty()) { val bitmap = BitmapFactory.decodeFile(filePath) - val thumbnail = ThumbnailUtils.extractThumbnail(bitmap, width, height) - Result.success(BitmapDrawable(thumbnail)) + if (bitmap != null) { + val thumbnail = ThumbnailUtils.extractThumbnail(bitmap, width, height) + Result.success(BitmapDrawable(thumbnail)) + } else { + Result.failure(Exception("Unable to create an image")) + } } else { Result.failure(Exception("Attachment is not loaded")) } From 4912f72c0ea9ffaeec7307cd8ab50cb602e4a6fe Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Mon, 12 Feb 2024 17:04:55 -0800 Subject: [PATCH 4/4] updated with latest api design --- .../featureforms/api/AttachmentFormElement.kt | 106 +++++++----------- 1 file changed, 40 insertions(+), 66 deletions(-) diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/api/AttachmentFormElement.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/api/AttachmentFormElement.kt index b71f358f3..44ef5b971 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/api/AttachmentFormElement.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/api/AttachmentFormElement.kt @@ -16,6 +16,7 @@ package com.arcgismaps.toolkit.featureforms.api +import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.drawable.BitmapDrawable @@ -23,7 +24,9 @@ import android.media.ThumbnailUtils import com.arcgismaps.LoadStatus import com.arcgismaps.Loadable import com.arcgismaps.data.ArcGISFeature +import com.arcgismaps.data.ArcGISFeatureTable import com.arcgismaps.data.Attachment +import com.arcgismaps.toolkit.featureforms.api.AttachmentFormElement.Companion.createOrNull import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -33,10 +36,8 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import java.io.File -import java.io.FileInputStream import java.io.FileOutputStream import java.nio.ByteBuffer -import android.content.Context /** * A FormElement type representing an Attachment. Use the factory method [createOrNull] to create @@ -66,78 +67,48 @@ internal class AttachmentFormElement private constructor( private val mutex = Mutex() /** - * Adds the specified [data] as an attachment with the specified [name] and content [contentType]. - * This method is thread-safe. + * Adds the specified [bitmapDrawable] as an attachment with the specified [name]. */ suspend fun addAttachment( name: String, contentType: String, - data: ByteArray + bitmapDrawable: BitmapDrawable ): Result { - mutex.withLock { - val attachment = feature.addAttachment(name, contentType, data).getOrNull() - return if (attachment != null) { - val formAttachment = FormAttachment(attachment, filesDir) - formAttachment.load() - _attachments.add(formAttachment) - Result.success(formAttachment) - } else { - Result.failure(Exception("Unable to create attachment")) - } + val byteArray = bitmapDrawable.bitmap.toByteArray() + val attachment = feature.addAttachment(name, contentType, byteArray).getOrNull() + return if (attachment != null) { + val formAttachment = FormAttachment(attachment, filesDir) + Result.success(formAttachment) + } else { + Result.failure(Exception("Unable to create attachment")) } } /** - * Loads and adds the specified [uri] as a [File] as an attachment with the specified [name]. + * Deletes the specified [attachment]. */ - suspend fun addAttachment( - name: String, - contentType: String, - uri: String - ): Result { - try { - var bytes: ByteArray? - FileInputStream(File((uri))).use { - bytes = it.readBytes() - } - bytes?.let { - return addAttachment(name, contentType, it) - } - return Result.failure(Exception("Unable to read from $uri")) - } catch (ex: Exception) { - if (ex is CancellationException) { - throw ex - } - return Result.failure(ex) + suspend fun deleteAttachment(attachment: FormAttachment): Result { + feature.deleteAttachment(attachment.attachment).onFailure { + return Result.failure(it) } + return Result.success(Unit) } /** - * Adds the specified [bitmapDrawable] as an attachment with the specified [name]. - */ - suspend fun addAttachment( - name: String, - contentType: String, - bitmapDrawable: BitmapDrawable - ): Result { - val byteArray = bitmapDrawable.bitmap.toByteArray() - return addAttachment(name, contentType, byteArray) - } - - /** - * Deletes the specified [attachment]. This method is thread-safe. + * Fetches the Attachments from the feature and populates [attachments] property. This method + * is thread safe. */ - suspend fun deleteAttachment(attachment: FormAttachment): Result { + suspend fun fetchAttachments(): Result { mutex.withLock { - if (_attachments.contains(attachment)) { - feature.deleteAttachment(attachment.attachment).onFailure { - return Result.failure(it) - } - _attachments.remove(attachment) - return Result.success(Unit) - } else { - return Result.failure(NoSuchElementException()) + val featureAttachments = feature.fetchAttachments().onFailure { + return Result.failure(it) + }.getOrNull()!! + val formAttachments = featureAttachments.map { + FormAttachment(it, filesDir) } + _attachments.clear() + _attachments.addAll(formAttachments) + return Result.success(Unit) } } @@ -153,14 +124,12 @@ internal class AttachmentFormElement private constructor( */ suspend fun createOrNull(feature: ArcGISFeature, filesDir: String): AttachmentFormElement? { feature.load().onFailure { return null } - val featureAttachments = feature.fetchAttachments().onFailure { - return null - }.getOrNull()!! - val formAttachments = featureAttachments.map { - FormAttachment(it, filesDir) - } - return AttachmentFormElement(feature, filesDir).apply { - _attachments.addAll(formAttachments) + val featureTable = feature.featureTable as? ArcGISFeatureTable ?: return null + featureTable.load() + return if (featureTable.hasAttachments) { + AttachmentFormElement(feature, filesDir) + } else { + null } } } @@ -184,7 +153,8 @@ internal class FormAttachment( var isLocal: Boolean = false private set - val name: String = attachment.name + var name: String = attachment.name + private set val size: Int = attachment.size @@ -221,6 +191,10 @@ internal class FormAttachment( } } + fun setName(name: String) { + this.name = name + } + override fun cancelLoad() { /** does nothing in this mock api **/ }