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

Ks/making image pulling more efficient #51

Merged
merged 2 commits into from
Feb 9, 2024
Merged
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 @@ -7,12 +7,18 @@ import androidx.compose.ui.graphics.asImageBitmap
import com.hierynomus.smbj.share.File

actual fun getBitmapFromFile(file: File, size: Int): ImageBitmap? {

val options = BitmapFactory.Options()
options.inSampleSize = 2
BitmapFactory.decodeStream(file.inputStream, null, options)?.let {
val dimensions = getScaledDimensions(it.width, it.height, size)
return Bitmap.createScaledBitmap(it, dimensions.first, dimensions.second, false)
.asImageBitmap()
}
return null
options.inJustDecodeBounds = true
BitmapFactory.decodeStream(file.inputStream, null, options)

val height: Int = options.outHeight
val width: Int = options.outWidth
val dimensions = getScaledDimensions(width, height, size)
val heightRatio: Int = Math.round(height.toFloat() / dimensions.second.toFloat())
val widthRatio: Int = Math.round(width.toFloat() / dimensions.first.toFloat())
options.inSampleSize = if (heightRatio < widthRatio) heightRatio else widthRatio

options.inJustDecodeBounds = false
return BitmapFactory.decodeStream(file.inputStream, null, options)?.asImageBitmap()
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,73 +14,24 @@ class RetrieveImageUseCase(
private val logger: Logger,
) {

private val IMAGE_SIZE_SMALL = 64
private val IMAGE_SIZE_MEDIUM = 256
private val IMAGE_SIZE_LARGE = 512
private val IMAGE_SIZE_EXTRA_LARGE = 1024

suspend operator fun invoke(
directory: ImageDirectory,
callback: suspend (State<ImageBitmap>) -> Unit,
) {
imageSize: Int,
): ImageBitmap? {
val imageName = "\"${directory.details.fullPath}\""
logger.i { "Starting to get Image $imageName" }
callback(State.LOADING)

var workingWidth: Int = 0
var updatedImage: Boolean = false

imageCacheDataSource.getImage(directory.details)?.let {
logger.i { "$imageName found in cache, using that" }
callback(State.SUCCESS(it))
workingWidth = it.width
}
logger.i { "Getting Image Bitmap from File $imageName" }
if (workingWidth <= IMAGE_SIZE_SMALL) {
logger.i { "Getting Small Bitmap for $imageName" }

val smallImageBitmap = downloadAndStoreImage(directory, IMAGE_SIZE_SMALL)
callback(smallImageBitmap.asState)
workingWidth = IMAGE_SIZE_SMALL
updatedImage = true
}
if (workingWidth <= IMAGE_SIZE_MEDIUM) {
logger.i { "Getting Medium Image" }
val mediumImageBitmap = downloadAndStoreImage(directory, IMAGE_SIZE_MEDIUM)
callback(mediumImageBitmap.asState)
workingWidth = IMAGE_SIZE_MEDIUM
updatedImage = true
return it
}
if (workingWidth <= IMAGE_SIZE_LARGE) {
logger.i { "Getting Large Image" }
val mediumImageBitmap = downloadAndStoreImage(directory, IMAGE_SIZE_LARGE)
callback(mediumImageBitmap.asState)
workingWidth = IMAGE_SIZE_LARGE
updatedImage = true
}
if (workingWidth <= IMAGE_SIZE_EXTRA_LARGE || !updatedImage) {
logger.i { "Getting Extra Large Image" }
val mediumImageBitmap = downloadAndStoreImage(directory, IMAGE_SIZE_EXTRA_LARGE)
callback(mediumImageBitmap.asState)
workingWidth = IMAGE_SIZE_EXTRA_LARGE
}
}

private fun downloadAndStoreImage(
directory: ImageDirectory,
bitmapSize: Int,
): ImageBitmap? {
logger.i { "Getting Image Bitmap from File ${directory.name}" }
val imageBitmap: ImageBitmap? = directory.image?.getImageBitmap(bitmapSize)
val imageBitmap: ImageBitmap? = directory.image?.getImageBitmap(imageSize)
imageBitmap?.let {
logger.i { "Caching new Image ${directory.name}" }
imageCacheDataSource.saveImage(directory.details, it)
}
return imageBitmap
}

private val ImageBitmap?.asState: State<ImageBitmap>
get() = this?.let {
State.SUCCESS(it)
} ?: State.ERROR("No Image Found")
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import com.kevinschildhorn.fotopresenter.UseCaseFactory
import kotlinx.coroutines.cancelChildren

interface ImageViewModel {
var scope: CoroutineScope?
Expand All @@ -41,7 +42,6 @@ class DefaultImageViewModel(private val logger: Logger? = null) : ImageViewModel
private val _uiState = MutableStateFlow(ImageScreenState())
override var scope: CoroutineScope? = null
override val imageUiState: StateFlow<ImageScreenState> = _uiState.asStateFlow()
private val jobs: MutableList<Job> = mutableListOf<Job>()

override fun setImageDirectories(directories: List<ImageDirectory>) {
_uiState.update { it.copy(imageDirectories = directories) }
Expand Down Expand Up @@ -83,10 +83,7 @@ class DefaultImageViewModel(private val logger: Logger? = null) : ImageViewModel

override fun cancelImageJobs() {
logger?.d { "Cancelling Image Jobs" }
jobs.forEach {
it.cancel()
}
jobs.clear()
scope?.coroutineContext?.cancelChildren()
}

private fun updateSelectedImage() {
Expand All @@ -106,14 +103,9 @@ class DefaultImageViewModel(private val logger: Logger? = null) : ImageViewModel
scope?.launch(Dispatchers.Default) {
val retrieveImagesUseCase = UseCaseFactory.retrieveImageUseCase
logger?.d { "Retrieving Image" }
retrieveImagesUseCase(imageDirectory) { newState: State<ImageBitmap> ->
logger?.d { "Image State Updated $newState" }
newState.value?.let { imageBitmap ->
_uiState.update { it.copy(selectedImage = imageBitmap) }
}
}
}?.let {
jobs.add(it)

val imageBitmap = retrieveImagesUseCase(imageDirectory, imageSize = 1024)
_uiState.update { it.copy(selectedImage = imageBitmap) }
}
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package com.kevinschildhorn.fotopresenter.ui.screens.directory

import co.touchlab.kermit.Logger
import com.kevinschildhorn.fotopresenter.UseCaseFactory
import com.kevinschildhorn.fotopresenter.data.Directory
import com.kevinschildhorn.fotopresenter.data.DirectoryContents
import com.kevinschildhorn.fotopresenter.data.ImageDirectory
import com.kevinschildhorn.fotopresenter.data.ImageSlideshowDetails
import com.kevinschildhorn.fotopresenter.data.PlaylistDetails
import com.kevinschildhorn.fotopresenter.data.State
import com.kevinschildhorn.fotopresenter.UseCaseFactory
import com.kevinschildhorn.fotopresenter.data.network.NetworkHandlerException
import com.kevinschildhorn.fotopresenter.data.repositories.PlaylistRepository
import com.kevinschildhorn.fotopresenter.domain.image.RetrieveImageUseCase
import com.kevinschildhorn.fotopresenter.extension.addPath
import com.kevinschildhorn.fotopresenter.extension.navigateBackToPathAtIndex
import com.kevinschildhorn.fotopresenter.ui.SortingType
Expand All @@ -17,13 +19,18 @@ import com.kevinschildhorn.fotopresenter.ui.screens.common.ActionSheetContext
import com.kevinschildhorn.fotopresenter.ui.screens.common.DefaultImageViewModel
import com.kevinschildhorn.fotopresenter.ui.screens.common.ImageViewModel
import com.kevinschildhorn.fotopresenter.ui.screens.playlist.PlaylistViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import kotlinx.datetime.Clock
import org.koin.core.component.KoinComponent

class DirectoryViewModel(
Expand All @@ -33,13 +40,16 @@ class DirectoryViewModel(
ImageViewModel by DefaultImageViewModel(logger),
KoinComponent {

private val slideshowScope: CoroutineScope = viewModelScope + Dispatchers.IO
private val imageScope: CoroutineScope = viewModelScope + Dispatchers.IO

private val _uiState = MutableStateFlow(DirectoryScreenState())
val uiState: StateFlow<DirectoryScreenState> = _uiState.asStateFlow()

private val _directoryContentsState = MutableStateFlow(DirectoryContents())

// Indexes of all Downloaded images
private val downloadedImageSet: MutableSet<Int> = mutableSetOf()
private val jobs: MutableList<Job> = mutableListOf<Job>()

private val currentPath: String
get() = uiState.value.currentPath
Expand All @@ -48,7 +58,7 @@ class DirectoryViewModel(
get() = uiState.value.selectedDirectory?.actionSheetContexts ?: emptyList()

init {
setImageScope(viewModelScope)
setImageScope(viewModelScope + Dispatchers.Default)
}

fun refreshScreen() {
Expand Down Expand Up @@ -84,13 +94,12 @@ class DirectoryViewModel(
logger.d { "Finding Folder" }
_directoryContentsState.value.folders.find { it.id == id }?.let {
logger.d { "Folder found, starting to retrieve images" }
val job = viewModelScope.launch(Dispatchers.Default) {
slideshowScope.launch {
val retrieveImagesUseCase = UseCaseFactory.retrieveImageDirectoriesUseCase
val images = retrieveImagesUseCase(it.details)
logger.v { "Retrieved images, copying them to state" }
_uiState.update { it.copy(slideshowDetails = ImageSlideshowDetails(images)) }
}
jobs.add(job)
}
} ?: run {
logger.w { "No Directory Selected!" }
Expand Down Expand Up @@ -136,7 +145,7 @@ class DirectoryViewModel(
logger.i { "Changing directory to path $path" }

cancelJobs()
viewModelScope.launch(Dispatchers.Default) {
slideshowScope.launch(Dispatchers.Default) {
val changeDirectoryUseCase = UseCaseFactory.changeDirectoryUseCase
try {
logger.i { "Getting New Path" }
Expand Down Expand Up @@ -171,7 +180,7 @@ class DirectoryViewModel(
private fun updateDirectories() {
logger.i { "Updating Directories" }
_uiState.update { it.copy(state = UiState.LOADING) }
val job = viewModelScope.launch(Dispatchers.Default) {
viewModelScope.launch(Dispatchers.Default) {
val retrieveDirectoryUseCase = UseCaseFactory.retrieveDirectoryContentsUseCase

logger.i { "Getting Directory Contents" }
Expand All @@ -183,34 +192,45 @@ class DirectoryViewModel(
logger.i { "Current State ${uiState.value.state}" }
updatePhotos()
}
jobs.add(job)
}

private fun updatePhotos() {
val count = imageUiState.value.imageDirectories.count()
downloadedImageSet.clear()

_uiState.update { it.copy(totalImageCount = count, currentImageCount = 0) }

logger.i { "Updating Photos" }
imageUiState.value.imageDirectories.forEachIndexed { index, imageDirectory ->
val job = viewModelScope.launch(Dispatchers.Default) {
val retrieveImagesUseCase = UseCaseFactory.retrieveImageUseCase

retrieveImagesUseCase(imageDirectory) { newState ->

downloadedImageSet.add(index)
_uiState.update {
it.copyImageState(
imageDirectory.id,
state = newState,
).copy(
currentImageCount = downloadedImageSet.size
)
imageScope.launch {
val startTime = Clock.System.now().toEpochMilliseconds()
logger.i { "Updating Photos" }
val retrieveImagesUseCase: RetrieveImageUseCase = UseCaseFactory.retrieveImageUseCase
val imageDirectories: List<ImageDirectory> = imageUiState.value.imageDirectories
imageDirectories.mapIndexed{ index, imageDirectory ->
async {
retrieveImagesUseCase(
imageDirectory,
imageSize = 512, // TODO: Change
)?.let { newImage ->
logger.i { "Downloaded Image at index $index" }
downloadedImageSet.add(index)

_uiState.update {
it.copyImageState(
imageDirectory.id,
state = State.SUCCESS(newImage),
).copy(
currentImageCount = downloadedImageSet.size
)
}

if (_uiState.value.currentImageCount == _uiState.value.totalImageCount) {
val endTime = Clock.System.now().toEpochMilliseconds()
val difference: Float = (endTime.toFloat() - startTime.toFloat()) / 1000
logger.i { "Downloading all images took $difference seconds" }
}
}
}
}
jobs.add(job)
}.awaitAll()

// TODO: STORE LARGEST IMAGES
}
}

Expand Down Expand Up @@ -279,10 +299,8 @@ class DirectoryViewModel(
private fun cancelJobs() {
logger.d { "Cancelling Jobs!" }
cancelImageJobs()
jobs.forEach {
it.cancel()
}
jobs.clear()
slideshowScope.coroutineContext.cancelChildren()
imageScope.coroutineContext.cancelChildren()
logger.v { "Finished Cancelling Jobs!" }

}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.kevinschildhorn.fotopresenter.data.network

import co.touchlab.kermit.Logger
import com.hierynomus.msdtyp.AccessMask
import com.hierynomus.msfscc.FileAttributes
import com.hierynomus.mssmb2.SMB2CreateDisposition
Expand All @@ -25,7 +26,8 @@ object SMBJHandler : NetworkHandler {
private var connection: Connection? = null
private var session: Session? = null
private var share: DiskShare? = null
private val metaDataName: String = "FotoMetaData.json"
private const val metaDataName: String = "FotoMetaData.json"
private val logger = Logger.withTag("SMBJHandler")

private val accessMask: Set<AccessMask> =
setOf(
Expand All @@ -52,11 +54,13 @@ object SMBJHandler : NetworkHandler {
val session: Session? = connection?.authenticate(context)
share = session?.connectShare(credentials.sharedFolder) as? DiskShare
if (share == null) {
logger.e { "Failed To Connect, shared was null" }
disconnect()
return false
}
}
} catch (e: Exception) {
logger.e(e) { "Failed To Connect" }
disconnect()
return false
}
Expand Down
Loading