Skip to content

Commit 6dc53db

Browse files
authored
Forms: Attachments API (#313)
* added mock api * Update AttachmentFormElement.kt * added check for creating bitmaps * updated with latest api design
1 parent 14afde8 commit 6dc53db

File tree

1 file changed

+241
-0
lines changed

1 file changed

+241
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
/*
2+
* Copyright 2024 Esri
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.arcgismaps.toolkit.featureforms.api
18+
19+
import android.content.Context
20+
import android.graphics.Bitmap
21+
import android.graphics.BitmapFactory
22+
import android.graphics.drawable.BitmapDrawable
23+
import android.media.ThumbnailUtils
24+
import com.arcgismaps.LoadStatus
25+
import com.arcgismaps.Loadable
26+
import com.arcgismaps.data.ArcGISFeature
27+
import com.arcgismaps.data.ArcGISFeatureTable
28+
import com.arcgismaps.data.Attachment
29+
import com.arcgismaps.toolkit.featureforms.api.AttachmentFormElement.Companion.createOrNull
30+
import kotlinx.coroutines.CancellationException
31+
import kotlinx.coroutines.Dispatchers
32+
import kotlinx.coroutines.flow.MutableStateFlow
33+
import kotlinx.coroutines.flow.StateFlow
34+
import kotlinx.coroutines.flow.asStateFlow
35+
import kotlinx.coroutines.sync.Mutex
36+
import kotlinx.coroutines.sync.withLock
37+
import kotlinx.coroutines.withContext
38+
import java.io.File
39+
import java.io.FileOutputStream
40+
import java.nio.ByteBuffer
41+
42+
/**
43+
* A FormElement type representing an Attachment. Use the factory method [createOrNull] to create
44+
* an instance.
45+
*/
46+
internal class AttachmentFormElement private constructor(
47+
private val feature: ArcGISFeature,
48+
private val filesDir: String
49+
) {
50+
/**
51+
* The input user interface to use for the element.
52+
*/
53+
val input: AttachmentFormInput = AttachmentFormInput.AnyAttachmentFormInput
54+
55+
/**
56+
* True if the element is editable. False if the element is not editable.
57+
*/
58+
val isEditable: Boolean = feature.canEditAttachments
59+
60+
private val _attachments: MutableList<FormAttachment> = mutableListOf()
61+
62+
/**
63+
* Returns all the current attachments.
64+
*/
65+
val attachments: List<FormAttachment> = _attachments
66+
67+
private val mutex = Mutex()
68+
69+
/**
70+
* Adds the specified [bitmapDrawable] as an attachment with the specified [name].
71+
*/
72+
suspend fun addAttachment(
73+
name: String,
74+
contentType: String,
75+
bitmapDrawable: BitmapDrawable
76+
): Result<FormAttachment> {
77+
val byteArray = bitmapDrawable.bitmap.toByteArray()
78+
val attachment = feature.addAttachment(name, contentType, byteArray).getOrNull()
79+
return if (attachment != null) {
80+
val formAttachment = FormAttachment(attachment, filesDir)
81+
Result.success(formAttachment)
82+
} else {
83+
Result.failure(Exception("Unable to create attachment"))
84+
}
85+
}
86+
87+
/**
88+
* Deletes the specified [attachment].
89+
*/
90+
suspend fun deleteAttachment(attachment: FormAttachment): Result<Unit> {
91+
feature.deleteAttachment(attachment.attachment).onFailure {
92+
return Result.failure(it)
93+
}
94+
return Result.success(Unit)
95+
}
96+
97+
/**
98+
* Fetches the Attachments from the feature and populates [attachments] property. This method
99+
* is thread safe.
100+
*/
101+
suspend fun fetchAttachments(): Result<Unit> {
102+
mutex.withLock {
103+
val featureAttachments = feature.fetchAttachments().onFailure {
104+
return Result.failure(it)
105+
}.getOrNull()!!
106+
val formAttachments = featureAttachments.map {
107+
FormAttachment(it, filesDir)
108+
}
109+
_attachments.clear()
110+
_attachments.addAll(formAttachments)
111+
return Result.success(Unit)
112+
}
113+
}
114+
115+
companion object {
116+
117+
/**
118+
* Creates a new [AttachmentFormElement] from the give [feature].
119+
*
120+
* @param feature The [ArcGISFeature] to create the [AttachmentFormElement] from.
121+
* @param filesDir The directory to the cache any attachments. Use [Context.getFilesDir].
122+
*
123+
* @return Returns null if unable to create a [AttachmentFormElement].
124+
*/
125+
suspend fun createOrNull(feature: ArcGISFeature, filesDir: String): AttachmentFormElement? {
126+
feature.load().onFailure { return null }
127+
val featureTable = feature.featureTable as? ArcGISFeatureTable ?: return null
128+
featureTable.load()
129+
return if (featureTable.hasAttachments) {
130+
AttachmentFormElement(feature, filesDir)
131+
} else {
132+
null
133+
}
134+
}
135+
}
136+
}
137+
138+
/**
139+
* Represents an attachment belonging to a feature form. Wraps the Attachment object and adds additional
140+
* properties and methods to support displaying attachments in a feature form.
141+
*
142+
* The [FormAttachment] must be loaded before calling [createFullImage] or [createThumbnail].
143+
*/
144+
internal class FormAttachment(
145+
val attachment: Attachment,
146+
private val filesDir: String
147+
) : Loadable {
148+
val contentType: String = attachment.contentType
149+
150+
var filePath: String = ""
151+
private set
152+
153+
var isLocal: Boolean = false
154+
private set
155+
156+
var name: String = attachment.name
157+
private set
158+
159+
val size: Int = attachment.size
160+
161+
private val _loadStatus: MutableStateFlow<LoadStatus> = MutableStateFlow(LoadStatus.NotLoaded)
162+
override val loadStatus: StateFlow<LoadStatus> = _loadStatus.asStateFlow()
163+
164+
@Suppress("DEPRECATION")
165+
suspend fun createFullImage(): Result<BitmapDrawable> = withContext(Dispatchers.IO) {
166+
return@withContext if (filePath.isNotEmpty()) {
167+
val bitmap = BitmapFactory.decodeFile(filePath)
168+
if (bitmap != null) {
169+
Result.success(BitmapDrawable(bitmap))
170+
} else {
171+
Result.failure(Exception("Unable to create an image"))
172+
}
173+
} else {
174+
Result.failure(Exception("Attachment is not loaded"))
175+
}
176+
}
177+
178+
@Suppress("DEPRECATION")
179+
suspend fun createThumbnail(width: Int, height: Int): Result<BitmapDrawable> =
180+
withContext(Dispatchers.IO) {
181+
return@withContext if (filePath.isNotEmpty()) {
182+
val bitmap = BitmapFactory.decodeFile(filePath)
183+
if (bitmap != null) {
184+
val thumbnail = ThumbnailUtils.extractThumbnail(bitmap, width, height)
185+
Result.success(BitmapDrawable(thumbnail))
186+
} else {
187+
Result.failure(Exception("Unable to create an image"))
188+
}
189+
} else {
190+
Result.failure(Exception("Attachment is not loaded"))
191+
}
192+
}
193+
194+
fun setName(name: String) {
195+
this.name = name
196+
}
197+
198+
override fun cancelLoad() {
199+
/** does nothing in this mock api **/
200+
}
201+
202+
override suspend fun load(): Result<Unit> {
203+
_loadStatus.value = LoadStatus.Loading
204+
try {
205+
if (!attachment.hasFetchedData || filePath.isEmpty()) {
206+
val data = attachment.fetchData().getOrThrow()
207+
val dir = File("$filesDir/attachments")
208+
dir.mkdirs()
209+
val file = File(dir, attachment.name)
210+
file.createNewFile()
211+
FileOutputStream(file).use {
212+
it.write(data)
213+
}
214+
filePath = file.absolutePath
215+
isLocal = true
216+
}
217+
} catch (ex: Exception) {
218+
_loadStatus.value = LoadStatus.FailedToLoad(ex)
219+
if (ex is CancellationException) {
220+
throw ex
221+
}
222+
return Result.failure(ex)
223+
}
224+
_loadStatus.value = LoadStatus.Loaded
225+
return Result.success(Unit)
226+
}
227+
228+
override suspend fun retryLoad(): Result<Unit> {
229+
return load()
230+
}
231+
}
232+
233+
internal sealed class AttachmentFormInput {
234+
data object AnyAttachmentFormInput : AttachmentFormInput()
235+
}
236+
237+
internal fun Bitmap.toByteArray(): ByteArray {
238+
val byteBuffer = ByteBuffer.allocate(allocationByteCount)
239+
copyPixelsToBuffer(byteBuffer)
240+
return byteBuffer.array()
241+
}

0 commit comments

Comments
 (0)