Skip to content

Commit 9a83c87

Browse files
committed
fix: switch to stream implementation of base64url encoding
1 parent 6ec56e3 commit 9a83c87

File tree

5 files changed

+99
-12
lines changed

5 files changed

+99
-12
lines changed

app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ dependencies {
199199
implementation(libs.play.services.location)
200200
implementation(libs.firebase.messaging.ktx)
201201
implementation(libs.androidx.datastore.preferences)
202+
implementation(libs.firebase.crashlytics.buildtools)
202203

203204
testImplementation(libs.junit)
204205
globalTestImplementation(libs.androidx.junit)

app/src/main/java/com/github/se/travelpouch/model/documents/DocumentRepository.kt

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@ package com.github.se.travelpouch.model.documents
44
import android.util.Log
55
import com.github.se.travelpouch.model.FirebasePaths
66
import com.github.se.travelpouch.model.activity.Activity
7-
import com.google.android.gms.common.util.Base64Utils
87
import com.google.firebase.Timestamp
98
import com.google.firebase.auth.FirebaseAuth
9+
import com.google.firebase.crashlytics.buildtools.reloc.org.apache.commons.codec.binary.BaseNCodecOutputStream
1010
import com.google.firebase.firestore.DocumentSnapshot
1111
import com.google.firebase.firestore.FirebaseFirestore
1212
import com.google.firebase.functions.FirebaseFunctions
1313
import com.google.firebase.storage.FirebaseStorage
14+
import java.io.ByteArrayOutputStream
15+
import kotlin.io.encoding.Base64
16+
import kotlin.io.encoding.ExperimentalEncodingApi
17+
import kotlin.math.min
1418

1519
/** Interface for the DocumentRepository. */
1620
interface DocumentRepository {
@@ -30,7 +34,7 @@ interface DocumentRepository {
3034
bytes: ByteArray,
3135
format: DocumentFileFormat,
3236
onSuccess: () -> Unit,
33-
onFailure: () -> Int
37+
onFailure: (e: Exception) -> Unit
3438
)
3539
}
3640

@@ -128,20 +132,63 @@ class DocumentRepositoryFirestore(
128132
}
129133
}
130134

135+
/**
136+
* Encodes a byte array to a base64URL string using a stream.
137+
*
138+
* @param bytes The byte array to encode.
139+
* @return The base64 encoded string.
140+
*/
141+
@OptIn(ExperimentalEncodingApi::class)
142+
private fun base64StreamUploadEncoding(bytes: ByteArray): String? {
143+
val byteArrayOutputStream = ByteArrayOutputStream()
144+
val base64OutputStream =
145+
BaseNCodecOutputStream(
146+
byteArrayOutputStream,
147+
com.google.firebase.crashlytics.buildtools.reloc.org.apache.commons.codec.binary.Base64(
148+
true),
149+
true) // urlSafe is a must
150+
151+
try {
152+
val buffer = ByteArray(8192)
153+
var offset = 0
154+
while (offset < bytes.size) {
155+
val chunkSize = min(buffer.size, bytes.size - offset)
156+
System.arraycopy(bytes, offset, buffer, 0, chunkSize)
157+
base64OutputStream.write(buffer, 0, chunkSize)
158+
offset += chunkSize
159+
}
160+
base64OutputStream.close() // closes but also flushes the stream
161+
return byteArrayOutputStream.toString("UTF-8")
162+
} catch (e: Exception) {
163+
Log.e("DocumentRepositoryFirestore", "Error encoding document", e)
164+
return null
165+
}
166+
}
167+
168+
/**
169+
* Uploads a document to the Firestore database.
170+
*
171+
* @param travelId The id of the travel the document is linked to.
172+
* @param bytes The content of the document.
173+
* @param format The format of the document.
174+
* @param onSuccess Callback function to be called when the document is uploaded successfully.
175+
* @param onFailure Callback function to be called when an error occurs.
176+
*/
131177
override fun uploadDocument(
132178
travelId: String,
133179
bytes: ByteArray,
134180
format: DocumentFileFormat,
135181
onSuccess: () -> Unit,
136-
onFailure: () -> Int
182+
onFailure: (e: Exception) -> Unit
137183
) {
138-
val bytes64 = Base64Utils.encodeUrlSafe(bytes)
184+
val encodedContent =
185+
base64StreamUploadEncoding(bytes) ?: return onFailure(Exception("Error encoding document"))
139186
val scanTimestamp = Timestamp.now().seconds
140187
functions
141188
.getHttpsCallable("storeDocument")
142189
.call(
143190
mapOf(
144-
"content" to bytes64,
191+
"content" to encodedContent,
145192
"fileFormat" to format.mimeType,
146193
"title" to "Scan $scanTimestamp",
147194
"travelId" to travelId,
@@ -152,7 +199,7 @@ class DocumentRepositoryFirestore(
152199
onSuccess()
153200
} else {
154201
Log.e("DocumentRepositoryFirestore", "Error uploading document", task.exception)
155-
onFailure()
202+
onFailure(task.exception!!)
156203
}
157204
}
158205
}

app/src/main/java/com/github/se/travelpouch/model/documents/DocumentViewModel.kt

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,13 @@ constructor(
156156
}
157157
}
158158

159-
fun uploadDocument(travelId: String, bytes: ByteArray, format: DocumentFileFormat) {
159+
fun uploadDocument(
160+
travelId: String,
161+
bytes: ByteArray,
162+
format: DocumentFileFormat,
163+
onSuccess: () -> Unit,
164+
onFailure: (e: Exception) -> Unit
165+
) {
160166
_isLoading.value = true // set as loading for spinner
161167
repository.uploadDocument(
162168
travelId,
@@ -169,6 +175,7 @@ constructor(
169175
onFailure = {
170176
_isLoading.value = false // set as not loading
171177
Log.e("DocumentsViewModel", "Failed to upload Document")
178+
onFailure(it)
172179
})
173180
}
174181

@@ -179,7 +186,12 @@ constructor(
179186
* @param selectedTravel The travel to which the file should be uploaded.
180187
* @param mimeType The mime type of the file.
181188
*/
182-
fun uploadFile(inputStream: InputStream?, selectedTravel: TravelContainer?, mimeType: String?) {
189+
fun uploadFile(
190+
inputStream: InputStream?,
191+
selectedTravel: TravelContainer?,
192+
mimeType: String?,
193+
onFailure: (e: Exception) -> Unit
194+
) {
183195
if (inputStream == null) {
184196
Log.e("DocumentViewModel", "No input stream")
185197
return
@@ -198,7 +210,15 @@ constructor(
198210
inputStream.copyTo(byteArrayOutputStream)
199211
val bytes: ByteArray = byteArrayOutputStream.toByteArray()
200212

201-
uploadDocument(travelId, bytes, format)
213+
uploadDocument(
214+
travelId,
215+
bytes,
216+
format,
217+
{},
218+
{
219+
Log.e("DocumentViewModel", "Failed to upload file", it)
220+
onFailure(it)
221+
})
202222
}
203223

204224
companion object {

app/src/main/java/com/github/se/travelpouch/ui/documents/DocumentList.kt

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import android.content.ContextWrapper
77
import android.net.Uri
88
import android.util.Log
99
import android.webkit.MimeTypeMap
10+
import android.widget.Toast
1011
import androidx.activity.ComponentActivity
1112
import androidx.activity.compose.rememberLauncherForActivityResult
1213
import androidx.activity.result.IntentSenderRequest
@@ -103,13 +104,27 @@ fun DocumentListScreen(
103104
val bytes = scanningResult.pages?.firstOrNull()?.imageUri?.toFile()?.readBytes()
104105
if (bytes != null && selectedTravel.value != null) {
105106
documentViewModel.uploadDocument(
106-
selectedTravel.value!!.fsUid, bytes, DocumentFileFormat.JPEG)
107+
selectedTravel.value!!.fsUid,
108+
bytes,
109+
DocumentFileFormat.JPEG,
110+
{},
111+
{
112+
Toast.makeText(context, "Document upload failed", Toast.LENGTH_SHORT)
113+
.show()
114+
})
107115
}
108116
} else if (size > 1 && selectedTravel.value != null) {
109117
scanningResult.pdf?.let { pdf ->
110118
val bytes = pdf.uri.toFile().readBytes()
111119
documentViewModel.uploadDocument(
112-
selectedTravel.value!!.fsUid, bytes, DocumentFileFormat.PDF)
120+
selectedTravel.value!!.fsUid,
121+
bytes,
122+
DocumentFileFormat.PDF,
123+
{},
124+
{
125+
Toast.makeText(context, "Document upload failed", Toast.LENGTH_SHORT)
126+
.show()
127+
})
113128
}
114129
}
115130
}
@@ -125,7 +140,9 @@ fun DocumentListScreen(
125140
context.contentResolver.openInputStream(uri),
126141
selectedTravel.value,
127142
MimeTypeMap.getSingleton()
128-
.getMimeTypeFromExtension(uri.path?.substringAfterLast(".") ?: ""))
143+
.getMimeTypeFromExtension(uri.path?.substringAfterLast(".") ?: "")) {
144+
Toast.makeText(context, "Document upload failed", Toast.LENGTH_SHORT).show()
145+
}
129146
else Log.d("DocumentList", "No file selected")
130147
}
131148

gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ playServicesMlkitDocumentScanner = "16.0.0-beta1"
7878
coil = "2.1.0"
7979
bouquet = "1.1.2"
8080
playServicesLocation = "21.3.0"
81+
firebaseCrashlyticsBuildtools = "3.0.2"
8182

8283
[libraries]
8384
androidx-concurrent-futures = { module = "androidx.concurrent:concurrent-futures", version.ref = "concurrentFutures" }
@@ -169,6 +170,7 @@ play-services-location = { group = "com.google.android.gms", name = "play-servic
169170

170171
#Permissions
171172
accompanist-permissions = { group = "com.google.accompanist", name = "accompanist-permissions", version = "0.30.0" }
173+
firebase-crashlytics-buildtools = { group = "com.google.firebase", name = "firebase-crashlytics-buildtools", version.ref = "firebaseCrashlyticsBuildtools" }
172174

173175
[plugins]
174176
androidApplication = { id = "com.android.application", version.ref = "agp" }

0 commit comments

Comments
 (0)