Skip to content
Draft
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
3 changes: 3 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,9 @@ dependencies {
implementation(libs.charts)
implementation(libs.haze)
implementation(libs.haze.materials)
// Image Loading
implementation(platform(libs.coil.bom))
implementation(libs.coil.compose)
// Compose Navigation
implementation(libs.navigation.compose)
androidTestImplementation(libs.navigation.testing)
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/java/to/bitkit/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import android.app.Application.ActivityLifecycleCallbacks
import android.os.Bundle
import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import coil3.ImageLoader
import coil3.SingletonImageLoader
import dagger.hilt.android.HiltAndroidApp
import to.bitkit.env.Env
import javax.inject.Inject
Expand All @@ -16,13 +18,17 @@ internal open class App : Application(), Configuration.Provider {
@Inject
lateinit var workerFactory: HiltWorkerFactory

@Inject
lateinit var imageLoader: ImageLoader

override val workManagerConfiguration
get() = Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()

override fun onCreate() {
super.onCreate()
SingletonImageLoader.setSafe { imageLoader }
currentActivity = CurrentActivity().also { registerActivityLifecycleCallbacks(it) }
Env.initAppStoragePath(filesDir.absolutePath)
}
Expand Down
58 changes: 0 additions & 58 deletions app/src/main/java/to/bitkit/data/PubkyImageCache.kt

This file was deleted.

50 changes: 50 additions & 0 deletions app/src/main/java/to/bitkit/data/PubkyImageFetcher.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package to.bitkit.data

import coil3.ImageLoader
import coil3.Uri
import coil3.decode.DataSource
import coil3.decode.ImageSource
import coil3.fetch.FetchResult
import coil3.fetch.Fetcher
import coil3.fetch.SourceFetchResult
import coil3.request.Options
import okio.Buffer
import org.json.JSONObject
import to.bitkit.services.PubkyService
import to.bitkit.utils.Logger

private const val TAG = "PubkyImageFetcher"
private const val PUBKY_SCHEME = "pubky://"

class PubkyImageFetcher(
private val uri: String,
private val options: Options,
private val pubkyService: PubkyService,
) : Fetcher {

override suspend fun fetch(): FetchResult {
val data = pubkyService.fetchFile(uri)
val blobData = resolveImageData(data)
val source = ImageSource(Buffer().apply { write(blobData) }, options.fileSystem)
return SourceFetchResult(source, null, dataSource = DataSource.NETWORK)
}

private suspend fun resolveImageData(data: ByteArray): ByteArray = runCatching {
val json = JSONObject(String(data))
val src = json.optString("src", "")
if (src.isNotEmpty() && src.startsWith(PUBKY_SCHEME)) {
Logger.debug("File descriptor found, fetching blob from '$src'", context = TAG)
pubkyService.fetchFile(src)
} else {
data
}
}.getOrDefault(data)

class Factory(private val pubkyService: PubkyService) : Fetcher.Factory<Uri> {
override fun create(data: Uri, options: Options, imageLoader: ImageLoader): Fetcher? {
val uri = data.toString()
if (!uri.startsWith(PUBKY_SCHEME)) return null
return PubkyImageFetcher(uri, options, pubkyService)
}
}
}
41 changes: 41 additions & 0 deletions app/src/main/java/to/bitkit/di/ImageModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package to.bitkit.di

import android.content.Context
import coil3.ImageLoader
import coil3.disk.DiskCache
import coil3.disk.directory
import coil3.memory.MemoryCache
import coil3.request.crossfade
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import to.bitkit.data.PubkyImageFetcher
import to.bitkit.services.PubkyService
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object ImageModule {

@Provides
@Singleton
fun provideImageLoader(
@ApplicationContext context: Context,
pubkyService: PubkyService,
): ImageLoader = ImageLoader.Builder(context)
.crossfade(true)
.components { add(PubkyImageFetcher.Factory(pubkyService)) }
.memoryCache {
MemoryCache.Builder()
.maxSizePercent(context, percent = 0.15)
.build()
}
.diskCache {
DiskCache.Builder()
.directory(context.cacheDir.resolve("pubky-images"))
.build()
}
.build()
}
40 changes: 13 additions & 27 deletions app/src/main/java/to/bitkit/repositories/PubkyRepo.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package to.bitkit.repositories

import android.graphics.Bitmap
import coil3.ImageLoader
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
Expand All @@ -18,8 +18,6 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.withContext
import org.json.JSONObject
import to.bitkit.data.PubkyImageCache
import to.bitkit.data.PubkyStore
import to.bitkit.data.keychain.Keychain
import to.bitkit.di.IoDispatcher
Expand All @@ -36,7 +34,7 @@ class PubkyRepo @Inject constructor(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val pubkyService: PubkyService,
private val keychain: Keychain,
private val imageCache: PubkyImageCache,
private val imageLoader: ImageLoader,
private val pubkyStore: PubkyStore,
) {
companion object {
Expand Down Expand Up @@ -251,37 +249,25 @@ class PubkyRepo @Inject constructor(
withContext(ioDispatcher) { pubkyService.forceSignOut() }
}.also {
runCatching { withContext(ioDispatcher) { keychain.delete(Keychain.Key.PAYKIT_SESSION.name) } }
runCatching { withContext(ioDispatcher) { imageCache.clear() } }
evictPubkyImages()
runCatching { withContext(ioDispatcher) { pubkyStore.reset() } }
_publicKey.update { null }
_profile.update { null }
_contacts.update { emptyList() }
_authState.update { PubkyAuthState.Idle }
}

fun cachedImage(uri: String): Bitmap? = imageCache.memoryImage(uri)

suspend fun fetchImage(uri: String): Result<Bitmap> = runCatching {
withContext(ioDispatcher) {
imageCache.image(uri)?.let { return@withContext it }

val data = pubkyService.fetchFile(uri)
val blobData = resolveImageData(data)
imageCache.decodeAndStore(blobData, uri).getOrThrow()
private fun evictPubkyImages() {
imageLoader.memoryCache?.let { cache ->
cache.keys.filter { it.key.startsWith(PUBKY_SCHEME) }.forEach { cache.remove(it) }
}
val imageUris = buildList {
_profile.value?.imageUrl?.let { add(it) }
addAll(_contacts.value.mapNotNull { it.imageUrl })
}
imageLoader.diskCache?.let { cache ->
imageUris.forEach { cache.remove(it) }
}
}

private suspend fun resolveImageData(data: ByteArray): ByteArray {
return runCatching {
val json = JSONObject(String(data))
val src = json.optString("src", "")
if (src.isNotEmpty() && src.startsWith(PUBKY_SCHEME)) {
Logger.debug("File descriptor found, fetching blob from: '$src'", context = TAG)
pubkyService.fetchFile(src)
} else {
data
}
}.getOrDefault(data)
}

private suspend fun cacheMetadata(profile: PubkyProfile) {
Expand Down
Loading
Loading