Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ internal class CommonHedvigBuildConstants(
override val urlGraphqlOctopus: String = appConfigUrlHolder.urlGraphqlOctopus(appBuildConfig.appFlavor)
override val urlBaseWeb: String = appConfigUrlHolder.urlBaseWeb(appBuildConfig.appFlavor)
override val urlOdyssey: String = appConfigUrlHolder.urlOdyssey(appBuildConfig.appFlavor)
override val urlHedvigGateway: String = appConfigUrlHolder.urlHedvigGateway(appBuildConfig.appFlavor)
override val urlBotService: String = appConfigUrlHolder.urlBotService(appBuildConfig.appFlavor)
override val urlClaimsService: String = appConfigUrlHolder.urlClaimsService(appBuildConfig.appFlavor)
override val deepLinkHosts: List<String> = appConfigUrlHolder.deepLinkHosts(appBuildConfig.appFlavor)
Expand Down Expand Up @@ -55,6 +56,7 @@ private interface UrlHolder {
fun urlGraphqlOctopus(flavor: Flavor): String
fun urlBaseWeb(flavor: Flavor): String
fun urlOdyssey(flavor: Flavor): String
fun urlHedvigGateway(flavor: Flavor): String
fun urlBotService(flavor: Flavor): String
fun urlClaimsService(flavor: Flavor): String
fun deepLinkHosts(flavor: Flavor): List<String>
Expand All @@ -80,6 +82,12 @@ private class AppConfigUrlHolder(private val appBuildConfig: AppBuildConfig) : U
Develop -> "https://odyssey.dev.hedvigit.com"
}

override fun urlHedvigGateway(flavor: Flavor): String = when (appBuildConfig.appFlavor) {
Production -> "https://gateway.hedvig.com"
Staging -> "https://gateway.dev.hedvigit.com"
Develop -> "https://gateway.dev.hedvigit.com"
}

override fun urlBotService(flavor: Flavor): String = when (appBuildConfig.appFlavor) {
Production -> "https://gateway.hedvig.com/bot-service"
Staging -> "https://gateway.dev.hedvigit.com/bot-service"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ interface HedvigBuildConstants {
*/
val urlOdyssey: String

/**
* The URL targeting the core hedvig gateway URL. To be used with APIs that return which sub-path they want the next
* request to go to
*/
val urlHedvigGateway: String

/**
* The URL targeting bot service backend
*/
Expand Down
38 changes: 23 additions & 15 deletions app/core/core-file-upload/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,24 +1,32 @@
plugins {
id("hedvig.android.library")
id("hedvig.multiplatform.library")
id("hedvig.multiplatform.library.android")
id("hedvig.gradle.plugin")
}

hedvig {
serialization()
compose()
}

dependencies {
api(libs.androidx.compose.foundation)
implementation(libs.arrow.core)
implementation(libs.koin.core)
implementation(libs.kotlinx.serialization.core)
implementation(libs.kotlinx.serialization.json)
implementation(libs.ktor.client.core)
implementation(projects.apolloOctopusPublic)
implementation(projects.coreBuildConstants)
implementation(projects.coreCommonPublic)
implementation(projects.coreResources)
implementation(projects.designSystemHedvig)
implementation(projects.networkClients)
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.arrow.core)
implementation(libs.koin.core)
implementation(libs.kotlinx.serialization.core)
implementation(libs.kotlinx.serialization.json)
implementation(libs.ktor.client.core)
implementation(libs.uri.kmp)
api(projects.coreBuildConstants)
api(projects.coreCommonPublic)
implementation(projects.networkClients)
}

androidMain.dependencies {
api(libs.androidx.compose.foundation)
implementation(projects.apolloOctopusPublic)
implementation(projects.coreResources)
implementation(projects.designSystemHedvig)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.hedvig.android.core.fileupload

import android.content.Context
import arrow.core.Either
import arrow.core.raise.either
import com.hedvig.android.core.common.ErrorMessage
import com.hedvig.android.logger.LogPriority
import com.hedvig.android.logger.logcat
import io.ktor.client.HttpClient
import io.ktor.client.request.prepareGet
import io.ktor.client.statement.bodyAsChannel
import io.ktor.utils.io.readAvailable
import java.io.File
import java.time.format.DateTimeFormatter
import kotlin.coroutines.cancellation.CancellationException
import kotlin.time.Clock
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toJavaLocalDateTime
import kotlinx.datetime.toLocalDateTime
import okio.buffer
import okio.sink

private const val FILE_NAME = "hedvig_"
private const val FILE_EXT = ".pdf"

internal class AndroidDownloadPdfUseCaseImpl(
private val context: Context,
private val clock: Clock,
private val httpClient: HttpClient,
) : DownloadPdfUseCase {
override suspend fun invoke(url: String): Either<ErrorMessage, DownloadedFile> = withContext(Dispatchers.IO) {
either {
try {
val now = DateTimeFormatter.ISO_DATE_TIME.format(
clock.now()
.toLocalDateTime(TimeZone.UTC)
.toJavaLocalDateTime(),
)

val downloadedFile = File(context.filesDir, FILE_NAME + now + FILE_EXT)

httpClient.prepareGet(url).execute { response ->
val channel = response.bodyAsChannel()
downloadedFile.sink().buffer().use { fileSink ->
val buffer = ByteArray(8192)
while (true) {
val bytesRead = channel.readAvailable(buffer)
if (bytesRead == -1) break
fileSink.write(buffer, 0, bytesRead)
}
}
}

DownloadedFile(
path = downloadedFile.absolutePath,
name = downloadedFile.name,
)
} catch (exception: Exception) {
if (exception is CancellationException) {
throw exception
}
logcat(LogPriority.ERROR, exception) { "Could not download pdf with: $exception" }
raise(ErrorMessage("Could not download pdf"))
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.hedvig.android.core.fileupload

import android.content.ContentResolver
import android.net.Uri as AndroidUri
import android.provider.OpenableColumns
import java.io.File
import kotlin.math.max
import kotlinx.io.IOException
import kotlinx.io.Source
import kotlinx.io.asSource
import kotlinx.io.buffered

class AndroidFile(
override val fileName: String,
override val mimeType: String,
override val description: String? = null,
private val contentResolver: ContentResolver,
private val androidUri: AndroidUri,
) : CommonFile {
override fun source(): Source {
val inputStream = contentResolver.openInputStream(androidUri)
?: throw IOException("Could not open input stream for uri:$androidUri")
return inputStream.asSource().buffered()
}

override fun getSize(): Long {
val statSize = contentResolver.openFileDescriptor(androidUri, "r")?.use {
it.statSize
} ?: -1

val sizeFromCursor = contentResolver.query(
androidUri,
arrayOf(OpenableColumns.SIZE),
null,
null,
null,
)?.use { cursor ->
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
if (cursor.moveToFirst()) cursor.getLong(sizeIndex) else null
} ?: -1

return max(statSize, sizeFromCursor)
}

companion object {
/**
* Creates an AndroidFile from a java.io.File
*/
fun fromFile(file: File, description: String? = null, mimeType: String = ""): CommonFile {
return object : CommonFile {
override val fileName: String = file.name
override val description: String? = description
override val mimeType: String = mimeType

override fun source(): Source {
return file.inputStream().asSource().buffered()
}

override fun getSize(): Long {
return file.length()
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package com.hedvig.android.core.fileupload

import android.content.ContentResolver
import android.net.Uri as AndroidUri
import android.provider.OpenableColumns
import android.webkit.MimeTypeMap
import com.eygraber.uri.Uri
import com.eygraber.uri.toAndroidUri
import com.hedvig.android.logger.logcat
import java.util.Locale
import kotlin.math.max

class AndroidFileService(
private val contentResolver: ContentResolver,
) : FileService {
override fun convertToCommonFile(uri: Uri): CommonFile {
val androidUri = uri.toAndroidUri()

return AndroidFile(
fileName = getFileName(uri) ?: "media",
mimeType = getMimeType(uri),
contentResolver = contentResolver,
androidUri = androidUri,
)
}

override fun getFileName(uri: Uri): String? {
val androidUri = uri.toAndroidUri()

if (androidUri.scheme == ContentResolver.SCHEME_CONTENT) {
val cursor = contentResolver.query(androidUri, null, null, null, null)
cursor.use { c ->
if (c?.moveToFirst() == true) {
val columnIndex = c.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (columnIndex >= 0) {
return c.getString(columnIndex)
}
}
}
}

val path = androidUri.path
return path?.substringAfterLast('/') ?: path
}

override fun getMimeType(uri: Uri): String {
val androidUri = uri.toAndroidUri()

if (androidUri.scheme == ContentResolver.SCHEME_CONTENT) {
val resolvedMimeType = contentResolver.getType(androidUri)
if (resolvedMimeType != null) {
return resolvedMimeType
}
}

return getMimeTypeFromPath(uri.toString())
}

override fun isFileSizeWithinBackendLimits(uri: Uri): Boolean {
val androidUri = uri.toAndroidUri()
val size = getFileSize(androidUri)
logcat {
"FileService: Size of the file: ${size / 1024 / 1024} Mb, " +
"Backend limit: ${BACKEND_CONTENT_SIZE_LIMIT / 1024 / 1024} Mb"
}
return size < BACKEND_CONTENT_SIZE_LIMIT
}

private fun getFileSize(androidUri: AndroidUri): Long {
val statSize = contentResolver.openFileDescriptor(androidUri, "r")?.use {
it.statSize
} ?: -1

val sizeFromCursor = contentResolver.query(
androidUri,
arrayOf(OpenableColumns.SIZE),
null,
null,
null,
)?.use { cursor ->
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
if (cursor.moveToFirst()) cursor.getLong(sizeIndex) else null
} ?: -1

logcat { "getFileSize for uri:$androidUri | statSize:$statSize | contentSize:$sizeFromCursor" }
return max(statSize, sizeFromCursor)
}

private fun getMimeTypeFromPath(path: String): String {
val fileExtension = MimeTypeMap.getFileExtensionFromUrl(path)
return MimeTypeMap.getSingleton()
.getMimeTypeFromExtension(fileExtension.lowercase(Locale.getDefault()))
?: ""
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.hedvig.android.core.fileupload

import android.content.Context
import com.hedvig.android.core.common.di.baseHttpClientQualifier
import io.ktor.client.HttpClient
import kotlin.time.Clock
import org.koin.core.module.Module
import org.koin.dsl.module

actual val fileUploadPlatformModule: Module = module {
single<FileService> {
AndroidFileService(
contentResolver = get<Context>().contentResolver,
)
}

single<DownloadPdfUseCase> {
AndroidDownloadPdfUseCaseImpl(
context = get<Context>(),
clock = get<Clock>(),
httpClient = get<HttpClient>(baseHttpClientQualifier),
)
}
}
Loading
Loading