Skip to content

Commit 55826b3

Browse files
committed
feat(data): update repositories and data sources
- Update CoreGenerationRepository - Add MediaStoreGateway implementations - Update GenerationResultLocalDataSource - Update AiGenerationResultMappers - Update PreferenceManagerImpl - Update FalAiGenerationRemoteDataSource - Update GenerationResultRepositoryImpl with tests - Update all generation repository implementations
1 parent 9a7c346 commit 55826b3

18 files changed

+244
-19
lines changed

data/src/main/java/dev/minios/pdaiv1/data/core/CoreGenerationRepository.kt

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package dev.minios.pdaiv1.data.core
22

33
import dev.minios.pdaiv1.core.imageprocessing.Base64ToBitmapConverter
4+
import dev.minios.pdaiv1.core.imageprocessing.blurhash.BlurHashEncoder
45
import dev.minios.pdaiv1.domain.datasource.GenerationResultDataSource
56
import dev.minios.pdaiv1.domain.entity.AiGenerationResult
67
import dev.minios.pdaiv1.domain.entity.MediaType
@@ -17,6 +18,7 @@ internal abstract class CoreGenerationRepository(
1718
private val preferenceManager: PreferenceManager,
1819
private val backgroundWorkObserver: BackgroundWorkObserver,
1920
private val mediaFileManager: MediaFileManager,
21+
private val blurHashEncoder: BlurHashEncoder,
2022
) : CoreMediaStoreRepository(preferenceManager, mediaStoreGateway, base64ToBitmapConverter) {
2123

2224
protected fun insertGenerationResult(ai: AiGenerationResult): Single<AiGenerationResult> {
@@ -25,21 +27,28 @@ internal abstract class CoreGenerationRepository(
2527
return localDataSource
2628
.insert(converted)
2729
.flatMap { id -> exportToMediaStore(ai).andThen(Single.just(ai.copy(id))) }
30+
.doOnSuccess { backgroundWorkObserver.postNewImageSignal() }
2831
}
2932
return Single.just(ai)
3033
}
3134

3235
/**
3336
* Converts base64 data to files before saving to database.
3437
* This prevents SQLiteBlobTooBigException for large images.
38+
* Also generates BlurHash for gallery placeholders.
3539
*/
3640
private fun AiGenerationResult.saveMediaToFiles(): AiGenerationResult {
3741
var mediaPath = this.mediaPath
3842
var inputMediaPath = this.inputMediaPath
43+
var blurHash = this.blurHash
3944

40-
// Convert main image base64 to file
45+
// Convert main image base64 to file and generate BlurHash
4146
if (image.isNotEmpty() && !mediaFileManager.isFilePath(image) && !mediaFileManager.isVideoUrl(image)) {
4247
mediaPath = mediaFileManager.migrateBase64ToFile(image, mediaType)
48+
// Generate BlurHash for gallery placeholder
49+
if (blurHash.isEmpty()) {
50+
blurHash = generateBlurHash(image)
51+
}
4352
}
4453

4554
// Convert input image base64 to file
@@ -52,6 +61,28 @@ internal abstract class CoreGenerationRepository(
5261
inputImage = "", // Clear base64 from database
5362
mediaPath = mediaPath,
5463
inputMediaPath = inputMediaPath,
64+
blurHash = blurHash,
5565
)
5666
}
67+
68+
/**
69+
* Generates BlurHash from base64 image string.
70+
* Returns empty string on failure.
71+
*/
72+
private fun generateBlurHash(base64Image: String): String {
73+
return try {
74+
val bytes = android.util.Base64.decode(base64Image, android.util.Base64.DEFAULT)
75+
val bitmap = android.graphics.BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
76+
if (bitmap != null) {
77+
// Scale down for faster encoding
78+
val scaledBitmap = android.graphics.Bitmap.createScaledBitmap(bitmap, 32, 32, true)
79+
val hash = blurHashEncoder.encodeSync(scaledBitmap)
80+
if (scaledBitmap != bitmap) scaledBitmap.recycle()
81+
bitmap.recycle()
82+
hash
83+
} else ""
84+
} catch (e: Exception) {
85+
""
86+
}
87+
}
5788
}

data/src/main/java/dev/minios/pdaiv1/data/gateway/mediastore/MediaStoreGatewayImpl.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,25 @@ internal class MediaStoreGatewayImpl(
8080
}
8181
}
8282

83+
override fun exportFromFile(fileName: String, sourceFile: File) {
84+
val contentValues = ContentValues().apply {
85+
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
86+
put(MediaStore.MediaColumns.MIME_TYPE, "image/png")
87+
put(MediaStore.MediaColumns.RELATIVE_PATH, "${Environment.DIRECTORY_DOWNLOADS}/PDAI/")
88+
}
89+
90+
val extVolumeUri: Uri = MediaStore.Files.getContentUri("external")
91+
val fileUri = context.contentResolver.insert(extVolumeUri, contentValues)
92+
93+
if (fileUri != null) {
94+
context.contentResolver.openOutputStream(fileUri, "wt")?.use { os ->
95+
sourceFile.inputStream().use { input ->
96+
input.copyTo(os, bufferSize = 8192)
97+
}
98+
}
99+
}
100+
}
101+
83102
override fun getInfo(): MediaStoreInfo {
84103
try {
85104
val extVolumeUri: Uri = MediaStore.Files.getContentUri("external")

data/src/main/java/dev/minios/pdaiv1/data/gateway/mediastore/MediaStoreGatewayOldImpl.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ internal class MediaStoreGatewayOldImpl : MediaStoreGateway {
3131
file.writeBytes(content)
3232
}
3333

34+
override fun exportFromFile(fileName: String, sourceFile: File) {
35+
val dirPath = Environment.getExternalStorageDirectory().path + DIR_PATH
36+
val dir = File(dirPath)
37+
if (!dir.exists()) dir.mkdirs()
38+
val destFile = File("${dirPath}/${fileName}.png")
39+
sourceFile.copyTo(destFile, overwrite = true)
40+
}
41+
3442
override fun getInfo(): MediaStoreInfo {
3543
val dirPath = Environment.getExternalStorageDirectory().path + DIR_PATH
3644
val dir = File(dirPath)

data/src/main/java/dev/minios/pdaiv1/data/local/GenerationResultLocalDataSource.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import dev.minios.pdaiv1.data.mappers.mapDomainToEntity
44
import dev.minios.pdaiv1.data.mappers.mapEntityToDomain
55
import dev.minios.pdaiv1.domain.datasource.GenerationResultDataSource
66
import dev.minios.pdaiv1.domain.entity.AiGenerationResult
7+
import dev.minios.pdaiv1.domain.entity.ThumbnailData
78
import dev.minios.pdaiv1.storage.db.persistent.dao.GenerationResultDao
89
import dev.minios.pdaiv1.storage.db.persistent.entity.GenerationResultEntity
910
import io.reactivex.rxjava3.core.Single
@@ -22,6 +23,14 @@ internal class GenerationResultLocalDataSource(
2223

2324
override fun queryAllIds(): Single<List<Long>> = dao.queryAllIds()
2425

26+
override fun queryAllIdsWithBlurHash(): Single<List<Pair<Long, String>>> = dao
27+
.queryAllIdsWithBlurHash()
28+
.map { list -> list.map { it.id to it.blurHash } }
29+
30+
override fun queryThumbnailInfoByIdList(idList: List<Long>): Single<List<ThumbnailData>> = dao
31+
.queryThumbnailInfoByIdList(idList)
32+
.map { list -> list.map { ThumbnailData(it.id, it.mediaPath, it.hidden, it.blurHash) } }
33+
2534
override fun queryPage(limit: Int, offset: Int) = dao
2635
.queryPage(limit, offset)
2736
.map(List<GenerationResultEntity>::mapEntityToDomain)
@@ -39,4 +48,10 @@ internal class GenerationResultLocalDataSource(
3948
override fun deleteByIdList(idList: List<Long>) = dao.deleteByIdList(idList)
4049

4150
override fun deleteAll() = dao.deleteAll()
51+
52+
override fun deleteAllUnliked() = dao.deleteAllUnliked()
53+
54+
override fun likeByIds(idList: List<Long>) = dao.likeByIds(idList)
55+
56+
override fun hideByIds(idList: List<Long>) = dao.hideByIds(idList)
4257
}

data/src/main/java/dev/minios/pdaiv1/data/mappers/AiGenerationResultMappers.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@ fun AiGenerationResult.mapDomainToEntity(): GenerationResultEntity = with(this)
2828
subSeedStrength = subSeedStrength,
2929
denoisingStrength = denoisingStrength,
3030
hidden = hidden,
31+
liked = liked,
3132
mediaPath = mediaPath,
3233
inputMediaPath = inputMediaPath,
3334
mediaType = mediaType.key,
3435
modelName = modelName,
36+
blurHash = blurHash,
3537
)
3638
}
3739
//endregion
@@ -60,10 +62,12 @@ fun GenerationResultEntity.mapEntityToDomain(): AiGenerationResult = with(this)
6062
subSeedStrength = subSeedStrength,
6163
denoisingStrength = denoisingStrength,
6264
hidden = hidden,
65+
liked = liked,
6366
mediaPath = mediaPath,
6467
inputMediaPath = inputMediaPath,
6568
mediaType = MediaType.parse(mediaType),
6669
modelName = modelName,
70+
blurHash = blurHash,
6771
)
6872
}
6973
//endregion

data/src/main/java/dev/minios/pdaiv1/data/preference/PreferenceManagerImpl.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -307,9 +307,9 @@ class PreferenceManagerImpl(
307307

308308
override var galleryGrid: Grid by preferences.delegates.complexInt(
309309
key = KEY_GALLERY_GRID,
310-
default = Grid.entries.first(),
311-
serialize = { grid -> grid.ordinal },
312-
deserialize = { index -> Grid.entries[index] },
310+
default = Grid.Fixed2,
311+
serialize = { grid -> grid.size },
312+
deserialize = { size -> Grid.fromSize(size) },
313313
onChanged = ::onPreferencesChanged,
314314
)
315315

data/src/main/java/dev/minios/pdaiv1/data/remote/FalAiGenerationRemoteDataSource.kt

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import dev.minios.pdaiv1.domain.feature.MediaFileManager
1515
import dev.minios.pdaiv1.network.api.falai.FalAiApi
1616
import dev.minios.pdaiv1.network.request.FalAiImageSize
1717
import dev.minios.pdaiv1.network.request.FalAiTextToImageRequest
18-
import dev.minios.pdaiv1.network.response.FalAiGenerationResponse
1918
import dev.minios.pdaiv1.network.response.FalAiImage
2019
import dev.minios.pdaiv1.network.response.FalAiQueueResponse
2120
import io.reactivex.rxjava3.core.Single
@@ -127,7 +126,6 @@ internal class FalAiGenerationRemoteDataSource(
127126

128127
when (mediaType) {
129128
MediaType.IMAGE -> {
130-
// Decode to bitmap and re-encode to ensure proper PNG format
131129
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
132130
?: throw IllegalStateException("Failed to decode image")
133131

@@ -136,33 +134,36 @@ internal class FalAiGenerationRemoteDataSource(
136134
mediaFileManager.saveMedia(outputStream.toByteArray(), MediaType.IMAGE)
137135
}
138136
MediaType.VIDEO -> {
139-
// Save video bytes directly
140137
mediaFileManager.saveMedia(bytes, MediaType.VIDEO)
141138
}
142139
}
143140
}
144141
}
145142

146-
@Deprecated("Use downloadAndSaveMedia instead", replaceWith = ReplaceWith("downloadAndSaveMedia(imageUrl, MediaType.IMAGE)"))
147143
private fun downloadAndConvertToBase64(imageUrl: String): Single<String> {
148144
return Single.fromCallable {
145+
debugLog("FalAi downloadAndConvertToBase64: starting download from $imageUrl")
149146
val request = Request.Builder().url(imageUrl).build()
150147
val response = httpClient.newCall(request).execute()
148+
debugLog("FalAi downloadAndConvertToBase64: response code=${response.code}")
151149

152150
if (!response.isSuccessful) {
153151
throw IllegalStateException("Failed to download image: ${response.code}")
154152
}
155153

156154
val bytes = response.body?.bytes()
157155
?: throw IllegalStateException("Empty response body")
156+
debugLog("FalAi downloadAndConvertToBase64: downloaded ${bytes.size} bytes")
158157

159-
// Decode to bitmap and re-encode to ensure proper format
160158
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
161159
?: throw IllegalStateException("Failed to decode image")
160+
debugLog("FalAi downloadAndConvertToBase64: decoded bitmap ${bitmap.width}x${bitmap.height}")
162161

163162
val outputStream = ByteArrayOutputStream()
164163
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
165-
Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP)
164+
val base64 = Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP)
165+
debugLog("FalAi downloadAndConvertToBase64: converted to base64, length=${base64.length}")
166+
base64
166167
}
167168
}
168169

@@ -219,6 +220,9 @@ internal class FalAiGenerationRemoteDataSource(
219220
debugLog("FalAi generateDynamic: url=$url, params=$requestBody")
220221

221222
return api.submitDynamicToQueue(url, requestBody)
223+
.doOnSuccess { queueResponse ->
224+
debugLog("FalAi submitDynamicToQueue response: requestId=${queueResponse.requestId}, statusUrl=${queueResponse.statusUrl}, images=${queueResponse.images?.size}, seed=${queueResponse.seed}")
225+
}
222226
.doOnError { t -> errorLog("FalAi submitDynamicToQueue error: ${t.message}") }
223227
.flatMap { queueResponse -> handleDynamicQueueResponse(queueResponse, endpoint, parameters) }
224228
}
@@ -310,9 +314,13 @@ internal class FalAiGenerationRemoteDataSource(
310314
responseSeed: String? = null,
311315
responsePrompt: String? = null,
312316
): Single<List<AiGenerationResult>> {
317+
debugLog("FalAi processImages: starting to process ${images.size} images")
313318
return io.reactivex.rxjava3.core.Observable.fromIterable(images)
314319
.flatMapSingle { image ->
320+
debugLog("FalAi processImages: downloading image from ${image.url}")
315321
downloadAndConvertToBase64(image.url)
322+
.doOnSuccess { debugLog("FalAi processImages: downloaded and converted image, base64 length=${it.length}") }
323+
.doOnError { t -> errorLog("FalAi processImages: error downloading image: ${t.message}") }
316324
.map { base64 ->
317325
createDynamicImageResult(
318326
endpoint = endpoint,
@@ -407,18 +415,12 @@ internal class FalAiGenerationRemoteDataSource(
407415
imageWidth: Int? = null,
408416
imageHeight: Int? = null,
409417
): AiGenerationResult {
410-
// Prefer response values over input parameters
411418
val prompt = responsePrompt ?: parameters["prompt"]?.toString() ?: ""
412419
val negativePrompt = parameters["negative_prompt"]?.toString() ?: ""
413-
414-
// Prefer actual image dimensions from response, then parameters, then defaults
415420
val width = imageWidth ?: extractWidth(parameters)
416421
val height = imageHeight ?: extractHeight(parameters)
417-
418422
val steps = (parameters["num_inference_steps"] as? Number)?.toInt() ?: 28
419423
val guidance = (parameters["guidance_scale"] as? Number)?.toFloat() ?: 3.5f
420-
421-
// Use seed from response if available (server may generate random seed)
422424
val seed = responseSeed ?: parameters["seed"]?.toString() ?: ""
423425

424426
val generationType = when (endpoint.category) {
@@ -454,7 +456,6 @@ internal class FalAiGenerationRemoteDataSource(
454456
}
455457

456458
private fun extractWidth(parameters: Map<String, Any?>): Int {
457-
// Handle image_size as object with width/height
458459
val imageSize = parameters["image_size"]
459460
return when (imageSize) {
460461
is Map<*, *> -> (imageSize["width"] as? Number)?.toInt() ?: 1024
@@ -473,7 +474,6 @@ internal class FalAiGenerationRemoteDataSource(
473474
}
474475

475476
private fun parseImageSizeDimension(sizeStr: String, isWidth: Boolean): Int {
476-
// Handle formats like "1024x1024", "landscape_16_9", etc.
477477
return if (sizeStr.contains("x")) {
478478
val parts = sizeStr.split("x")
479479
(if (isWidth) parts.getOrNull(0) else parts.getOrNull(1))
@@ -485,7 +485,6 @@ internal class FalAiGenerationRemoteDataSource(
485485

486486
companion object {
487487
private const val BASE_URL = "https://queue.fal.run/"
488-
private const val DEFAULT_MODEL = "fal-ai/flux-lora"
489488
private const val POLL_INTERVAL_MS = 2000L
490489
}
491490
}

data/src/main/java/dev/minios/pdaiv1/data/repository/GenerationResultRepositoryImpl.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ internal class GenerationResultRepositoryImpl(
3030

3131
override fun getAllIds() = localDataSource.queryAllIds()
3232

33+
override fun getAllIdsWithBlurHash() = localDataSource.queryAllIdsWithBlurHash()
34+
35+
override fun getThumbnailInfoByIds(idList: List<Long>) = localDataSource.queryThumbnailInfoByIdList(idList)
36+
3337
override fun getPage(limit: Int, offset: Int) = localDataSource.queryPage(limit, offset)
3438
.map { results -> results.map { it.loadMediaFromFiles() } }
3539

@@ -41,6 +45,8 @@ internal class GenerationResultRepositoryImpl(
4145
override fun getByIds(idList: List<Long>) = localDataSource.queryByIdList(idList)
4246
.map { results -> results.map { it.loadMediaFromFiles() } }
4347

48+
override fun getByIdsRaw(idList: List<Long>) = localDataSource.queryByIdList(idList)
49+
4450
override fun insert(result: AiGenerationResult): Single<Long> {
4551
val converted = result.saveMediaToFiles()
4652
return localDataSource
@@ -88,13 +94,26 @@ internal class GenerationResultRepositoryImpl(
8894
}
8995
.flatMapCompletable { localDataSource.deleteAll() }
9096

97+
override fun deleteAllUnliked(): Completable = localDataSource.deleteAllUnliked()
98+
9199
override fun toggleVisibility(id: Long): Single<Boolean> = localDataSource
92100
.queryById(id)
93101
.map { it.copy(hidden = !it.hidden) }
94102
.flatMap(localDataSource::insert)
95103
.flatMap { localDataSource.queryById(id) }
96104
.map(AiGenerationResult::hidden)
97105

106+
override fun toggleLike(id: Long): Single<Boolean> = localDataSource
107+
.queryById(id)
108+
.map { it.copy(liked = !it.liked) }
109+
.flatMap(localDataSource::insert)
110+
.flatMap { localDataSource.queryById(id) }
111+
.map(AiGenerationResult::liked)
112+
113+
override fun likeByIds(idList: List<Long>): Completable = localDataSource.likeByIds(idList)
114+
115+
override fun hideByIds(idList: List<Long>): Completable = localDataSource.hideByIds(idList)
116+
98117
override fun migrateBase64ToFiles(): Completable = localDataSource.queryAll()
99118
.flatMapCompletable { results ->
100119
val needsMigration = results.filter { result ->

data/src/main/java/dev/minios/pdaiv1/data/repository/HordeGenerationRepositoryImpl.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package dev.minios.pdaiv1.data.repository
22

33
import dev.minios.pdaiv1.core.imageprocessing.Base64ToBitmapConverter
4+
import dev.minios.pdaiv1.core.imageprocessing.blurhash.BlurHashEncoder
45
import dev.minios.pdaiv1.data.core.CoreGenerationRepository
56
import dev.minios.pdaiv1.domain.datasource.GenerationResultDataSource
67
import dev.minios.pdaiv1.domain.datasource.HordeGenerationDataSource
@@ -19,6 +20,7 @@ internal class HordeGenerationRepositoryImpl(
1920
preferenceManager: PreferenceManager,
2021
backgroundWorkObserver: BackgroundWorkObserver,
2122
mediaFileManager: MediaFileManager,
23+
blurHashEncoder: BlurHashEncoder,
2224
private val remoteDataSource: HordeGenerationDataSource.Remote,
2325
private val statusSource: HordeGenerationDataSource.StatusSource,
2426
) : CoreGenerationRepository(
@@ -28,6 +30,7 @@ internal class HordeGenerationRepositoryImpl(
2830
preferenceManager = preferenceManager,
2931
backgroundWorkObserver = backgroundWorkObserver,
3032
mediaFileManager = mediaFileManager,
33+
blurHashEncoder = blurHashEncoder,
3134
), HordeGenerationRepository {
3235

3336
override fun observeStatus() = statusSource.observe()

0 commit comments

Comments
 (0)