Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release: 1.7.4 #364

Merged
merged 10 commits into from
Aug 22, 2024
Prev Previous commit
Next Next commit
Get rid of glide & move thumbnail related logic to separate class (#329)
**Description:**
- Remove Glide and rewrite the thumbnail creation logic in Coil. 
- Move the thumbnail-related functionalities to a dedicated
`ThumbnailManager` class as discussed in the linked issue.

Closes #318

---------

Signed-off-by: starry-shivam <starry@krsh.dev>
  • Loading branch information
starry-shivam committed Aug 7, 2024
commit 276657a47e73ea2635d446312558ed8a008bee39
4 changes: 1 addition & 3 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -143,16 +143,14 @@ dependencies {
// Display OSS Licenses
implementation("com.github.leonlatsch:OssLicenseView:1.1.0")

// Glide
implementation("com.github.bumptech.glide:glide:4.16.0")

// Telephoto
implementation("me.saket.telephoto:zoomable-image-coil:0.12.1")

// Coil
val coilVersion = "2.7.0"
implementation("io.coil-kt:coil-compose:$coilVersion")
implementation("io.coil-kt:coil-gif:$coilVersion")
implementation("io.coil-kt:coil-video:$coilVersion")

// Exoplayer
implementation("com.google.android.exoplayer:exoplayer-core:2.19.1")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package dev.leonlatsch.photok.backup.domain
import dev.leonlatsch.photok.backup.data.BackupMetaData
import dev.leonlatsch.photok.backup.data.toDomain
import dev.leonlatsch.photok.model.database.entity.internalFileName
import dev.leonlatsch.photok.model.io.CreateThumbnailUseCase
import dev.leonlatsch.photok.model.repositories.PhotoRepository
import dev.leonlatsch.photok.security.EncryptionManager
import java.io.ByteArrayInputStream
Expand All @@ -30,6 +31,7 @@ import kotlin.coroutines.suspendCoroutine
class RestoreBackupV1 @Inject constructor(
private val encryptionManager: EncryptionManager,
private val photoRepository: PhotoRepository,
private val createThumbnailUseCase: CreateThumbnailUseCase,
) : RestoreBackupStrategy {
override suspend fun restore(
metaData: BackupMetaData,
Expand Down Expand Up @@ -66,7 +68,7 @@ class RestoreBackupV1 @Inject constructor(
photoRepository.createPhotoFile(newPhoto, photoBytesInputStream) != -1L

if (photoFileCreated) {
photoRepository.createThumbnail(newPhoto, photoBytes)
createThumbnailUseCase(newPhoto, photoBytes)
photoRepository.insert(newPhoto)
}

Expand Down
12 changes: 11 additions & 1 deletion app/src/main/java/dev/leonlatsch/photok/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package dev.leonlatsch.photok.di

import android.content.Context
import android.content.res.Resources
import androidx.room.Room
import com.google.gson.Gson
import com.google.gson.GsonBuilder
Expand All @@ -28,6 +29,8 @@ import dagger.hilt.components.SingletonComponent
import dev.leonlatsch.photok.gallery.ui.importing.SharedUrisStore
import dev.leonlatsch.photok.model.database.DATABASE_NAME
import dev.leonlatsch.photok.model.database.PhotokDatabase
import dev.leonlatsch.photok.model.io.EncryptedStorageManager
import dev.leonlatsch.photok.model.io.CreateThumbnailUseCase
import dev.leonlatsch.photok.security.EncryptionManager
import dev.leonlatsch.photok.settings.data.Config
import javax.inject.Singleton
Expand Down Expand Up @@ -71,7 +74,14 @@ object AppModule {
fun provideSharedUrisStore() = SharedUrisStore()

@Provides
fun provideResources(@ApplicationContext context: Context) = context.resources
@Singleton
fun provideCreateThumbnailUseCase(
@ApplicationContext context: Context,
encryptedStorageManager: EncryptedStorageManager
) = CreateThumbnailUseCase(context, encryptedStorageManager)

@Provides
fun provideResources(@ApplicationContext context: Context): Resources = context.resources

@Provides
fun provideGson(): Gson = GsonBuilder()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Copyright 2020-2024 Leon Latsch
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package dev.leonlatsch.photok.model.io

import android.content.Context
import android.graphics.Bitmap
import androidx.core.graphics.drawable.toBitmap
import coil.ImageLoader
import coil.decode.VideoFrameDecoder
import coil.request.ImageRequest
import coil.size.Size
import coil.transform.Transformation
import dev.leonlatsch.photok.model.database.entity.Photo
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber


/** Maximum size of the thumbnail in pixels */
private const val THUMBNAIL_SIZE = 128

/**
* Use case to create a thumbnail for a photo or video.
*
* @param context the application context
* @param encryptedStorageManager the [EncryptedStorageManager] to access the encrypted files
*
* @since 1.7.2
* @author Starry Shivam
*/
class CreateThumbnailUseCase(
private val context: Context,
private val encryptedStorageManager: EncryptedStorageManager
) {

// invoke operator to create thumbnail
suspend operator fun invoke(photo: Photo, obj: Any?): Result<Unit> {
return createThumbnail(photo, obj)
}

/**
* Creates a thumbnail for the given photo. If the photo is a video,
* it also creates a video preview.
*
* @param photo The photo object for which the thumbnail is to be created.
* @param obj The data for the image request. This could be a URL, file, or any other supported data type.
* @return A [Result] indicating the success or failure of the thumbnail creation.
*/
private suspend fun createThumbnail(
photo: Photo, obj: Any?
): Result<Unit> = withContext(Dispatchers.IO) {
val deferredResult = CompletableDeferred<Result<Unit>>()

val imageLoader = ImageLoader.Builder(context)
.components { add(VideoFrameDecoder.Factory()) }
.build()

val request = ImageRequest.Builder(context)
.data(obj)
.size(THUMBNAIL_SIZE)
.transformations(CenterCropTransformation())
.allowHardware(false)
.target(
onSuccess = { result ->
try {
// Lambda to compress & save bitmap and avoid code duplication.
val saveCompressedBitmap: (fileName: String) -> Unit = { fileName ->
encryptedStorageManager.internalOpenEncryptedFileOutput(fileName)
?.use { ops ->
result.toBitmap().compress(Bitmap.CompressFormat.JPEG, 100, ops)
}
}
// Create thumbnail
saveCompressedBitmap(photo.internalThumbnailFileName)
// If the photo is a video, create a video preview
if (photo.type.isVideo) {
saveCompressedBitmap(photo.internalVideoPreviewFileName)
}
deferredResult.complete(Result.success(Unit))
} catch (e: Exception) {
deferredResult.complete(Result.failure(e))
}
},
onError = {
Timber.e("Error creating thumbnail for ${photo.fileName}")
deferredResult.complete(
Result.failure(Exception("Error creating thumbnail for ${photo.fileName}"))
)
}
)
.build()

imageLoader.execute(request)
deferredResult.await()
}
}

class CenterCropTransformation : Transformation {

override val cacheKey: String = javaClass.name

override suspend fun transform(input: Bitmap, size: Size): Bitmap {
val minDim = minOf(input.width, input.height)
val startX = (input.width - minDim) / 2
val startY = (input.height - minDim) / 2

return Bitmap.createBitmap(input, startX, startY, minDim, minDim)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,14 @@ package dev.leonlatsch.photok.model.repositories

import android.app.Application
import android.content.ContentValues
import android.graphics.Bitmap
import android.net.Uri
import android.provider.MediaStore
import com.bumptech.glide.Glide
import dev.leonlatsch.photok.model.database.dao.AlbumDao
import dev.leonlatsch.photok.model.database.dao.PhotoDao
import dev.leonlatsch.photok.model.database.entity.Photo
import dev.leonlatsch.photok.model.database.entity.PhotoType
import dev.leonlatsch.photok.model.io.EncryptedStorageManager
import dev.leonlatsch.photok.model.io.CreateThumbnailUseCase
import dev.leonlatsch.photok.other.extensions.empty
import dev.leonlatsch.photok.other.extensions.lazyClose
import dev.leonlatsch.photok.other.getFileName
Expand All @@ -49,8 +48,9 @@ class PhotoRepository @Inject constructor(
private val photoDao: PhotoDao,
private val albumDao: AlbumDao,
private val encryptedStorageManager: EncryptedStorageManager,
private val createThumbnailUseCase: CreateThumbnailUseCase,
private val app: Application,
private val config: Config
private val config: Config,
) {

// region DATABASE
Expand Down Expand Up @@ -136,7 +136,7 @@ class PhotoRepository @Inject constructor(
*
* @return true, if everything worked
*/
suspend fun safeCreatePhoto(
private suspend fun safeCreatePhoto(
photo: Photo,
source: InputStream?,
origUri: Uri? = null
Expand All @@ -148,10 +148,7 @@ class PhotoRepository @Inject constructor(
photo.size = fileLen

if (origUri != null) {
createThumbnail(photo, origUri)
if (photo.type.isVideo) {
createVideoPreview(photo, origUri)
}
createThumbnailUseCase(photo, origUri)
}

val photoId = insert(photo)
Expand All @@ -177,44 +174,6 @@ class PhotoRepository @Inject constructor(
return fileLen
}

private fun createThumbnail(photo: Photo, sourceUri: Uri) =
internalCreateThumbnail(photo, sourceUri)

/**
* Create a thumbnail from raw bytes.
*/
fun createThumbnail(photo: Photo, bytes: ByteArray) =
internalCreateThumbnail(photo, bytes)

private fun internalCreateThumbnail(photo: Photo, obj: Any?) {
val thumbnail = Glide.with(app)
.asBitmap()
.load(obj)
.centerCrop()
.submit(THUMBNAIL_SIZE, THUMBNAIL_SIZE)
.get()

encryptedStorageManager.internalOpenEncryptedFileOutput(
photo.internalThumbnailFileName
)?.use {
thumbnail?.compress(Bitmap.CompressFormat.JPEG, 100, it)
}
}

private fun createVideoPreview(photo: Photo, sourceUri: Uri) {
val preview = Glide.with(app)
.asBitmap()
.load(sourceUri)
.submit()
.get()

encryptedStorageManager.internalOpenEncryptedFileOutput(
photo.internalVideoPreviewFileName
)?.use {
preview?.compress(Bitmap.CompressFormat.JPEG, 100, it)
}
}

// endregion

// region READ
Expand All @@ -231,30 +190,6 @@ class PhotoRepository @Inject constructor(
return null
}

/**
* Loads the full size thumbnail stored for this photo as a [ByteArray]
*/
fun loadThumbnail(photo: Photo): ByteArray? {
encryptedStorageManager.internalOpenEncryptedFileInput(photo.internalThumbnailFileName)
?.use {
return it.readBytes()
}

return null
}

/**
* Load the full size preview for a stored photo as a [ByteArray]
*/
fun loadVideoPreview(photo: Photo): ByteArray? {
encryptedStorageManager.internalOpenEncryptedFileInput(photo.internalVideoPreviewFileName)
?.use {
return it.readBytes()
}

return null
}

// endregion

// region DELETE
Expand Down Expand Up @@ -351,8 +286,4 @@ class PhotoRepository @Inject constructor(

// endregion
// endregion

companion object {
private const val THUMBNAIL_SIZE = 128
}
}
Loading