music: make caching thread safe
This commit is contained in:
parent
c3f9f0d80e
commit
b832ac8639
3 changed files with 53 additions and 38 deletions
|
@ -20,10 +20,14 @@ package org.oxycblt.auxio.image.stack.cache
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.concurrent.withLock
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
interface AppFiles {
|
interface AppFiles {
|
||||||
|
@ -34,8 +38,15 @@ interface AppFiles {
|
||||||
|
|
||||||
class AppFilesImpl @Inject constructor(@ApplicationContext private val context: Context) :
|
class AppFilesImpl @Inject constructor(@ApplicationContext private val context: Context) :
|
||||||
AppFiles {
|
AppFiles {
|
||||||
|
private val fileMutexes = mutableMapOf<String, Mutex>()
|
||||||
|
private val mapMutex = Mutex()
|
||||||
|
|
||||||
|
private suspend fun getMutexForFile(file: String): Mutex {
|
||||||
|
return mapMutex.withLock { fileMutexes.getOrPut(file) { Mutex() } }
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun read(file: String): InputStream? =
|
override suspend fun read(file: String): InputStream? =
|
||||||
withContext(context = Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
context.openFileInput(file)
|
context.openFileInput(file)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
|
@ -43,17 +54,27 @@ class AppFilesImpl @Inject constructor(@ApplicationContext private val context:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun write(file: String, inputStream: InputStream) =
|
override suspend fun write(file: String, inputStream: InputStream): Boolean =
|
||||||
withContext(context = Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
val fileMutex = getMutexForFile(file)
|
||||||
context.openFileOutput(file, Context.MODE_PRIVATE).use { fileOutputStream ->
|
|
||||||
inputStream.copyTo(fileOutputStream)
|
fileMutex.withLock {
|
||||||
|
val tempFile = File(context.filesDir, "$file.tmp")
|
||||||
|
val targetFile = File(context.filesDir, file)
|
||||||
|
|
||||||
|
try {
|
||||||
|
tempFile.outputStream().use { fileOutputStream ->
|
||||||
|
inputStream.copyTo(fileOutputStream)
|
||||||
|
}
|
||||||
|
|
||||||
|
tempFile.renameTo(targetFile)
|
||||||
|
true
|
||||||
|
} catch (e: IOException) {
|
||||||
|
tempFile.delete()
|
||||||
|
false
|
||||||
|
} finally {
|
||||||
|
inputStream.close()
|
||||||
}
|
}
|
||||||
true
|
|
||||||
} catch (e: IOException) {
|
|
||||||
false
|
|
||||||
} finally {
|
|
||||||
inputStream.close()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ import javax.inject.Singleton
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
interface StackModule {
|
interface StackModule {
|
||||||
@Binds fun appFiles(impl: AppFilesImpl): AppFiles
|
@Singleton @Binds fun appFiles(impl: AppFilesImpl): AppFiles
|
||||||
|
|
||||||
@Binds fun perceptualHash(perceptualHash: PerceptualHashImpl): PerceptualHash
|
@Binds fun perceptualHash(perceptualHash: PerceptualHashImpl): PerceptualHash
|
||||||
|
|
||||||
|
|
|
@ -19,56 +19,49 @@
|
||||||
package org.oxycblt.auxio.image.stack.cache
|
package org.oxycblt.auxio.image.stack.cache
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.math.min
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.image.extractor.Cover
|
||||||
|
|
||||||
interface CoverCache {
|
interface CoverCache {
|
||||||
suspend fun read(song: Song): InputStream?
|
suspend fun read(cover: Cover.Single): InputStream?
|
||||||
|
|
||||||
suspend fun write(song: Song, inputStream: InputStream): Boolean
|
suspend fun write(cover: Cover.Single, inputStream: InputStream): Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
class CoverCacheImpl
|
class CoverCacheImpl
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(private val storedCoversDao: StoredCoversDao, private val appFiles: AppFiles) :
|
||||||
private val storedCoversDao: StoredCoversDao,
|
CoverCache {
|
||||||
private val appFiles: AppFiles,
|
|
||||||
private val perceptualHash: PerceptualHash
|
|
||||||
) : CoverCache {
|
|
||||||
|
|
||||||
override suspend fun read(song: Song): InputStream? {
|
override suspend fun read(cover: Cover.Single): InputStream? {
|
||||||
val perceptualHash =
|
val perceptualHash =
|
||||||
storedCoversDao.getCoverFile(song.uid, song.lastModified) ?: return null
|
storedCoversDao.getCoverFile(cover.uid, cover.lastModified) ?: return null
|
||||||
|
|
||||||
return appFiles.read(fileName(perceptualHash))
|
return appFiles.read(fileName(perceptualHash))
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun write(song: Song, inputStream: InputStream): Boolean =
|
override suspend fun write(cover: Cover.Single, inputStream: InputStream): Boolean =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val bitmap = BitmapFactory.decodeStream(inputStream)
|
val available = inputStream.available()
|
||||||
val perceptualHash = perceptualHash.hash(bitmap)
|
val skip = min(available / 2L, available - COVER_KEY_SAMPLE.toLong())
|
||||||
|
inputStream.skip(skip)
|
||||||
|
val bytes = ByteArray(COVER_KEY_SAMPLE)
|
||||||
|
inputStream.read(bytes)
|
||||||
|
inputStream.reset()
|
||||||
|
|
||||||
// Compress bitmap down to webp into another inputstream
|
@OptIn(ExperimentalStdlibApi::class) val perceptualHash = bytes.toHexString()
|
||||||
val compressedStream =
|
val writeSuccess = appFiles.write(fileName(perceptualHash), inputStream)
|
||||||
ByteArrayOutputStream().use { outputStream ->
|
|
||||||
bitmap.compress(COVER_CACHE_FORMAT, 80, outputStream)
|
|
||||||
ByteArrayInputStream(outputStream.toByteArray())
|
|
||||||
}
|
|
||||||
|
|
||||||
val writeSuccess = appFiles.write(fileName(perceptualHash), compressedStream)
|
|
||||||
|
|
||||||
if (writeSuccess) {
|
if (writeSuccess) {
|
||||||
storedCoversDao.setCoverFile(
|
storedCoversDao.setCoverFile(
|
||||||
StoredCover(
|
StoredCover(
|
||||||
uid = song.uid,
|
uid = cover.uid,
|
||||||
lastModified = song.lastModified,
|
lastModified = cover.lastModified,
|
||||||
perceptualHash = perceptualHash))
|
perceptualHash = perceptualHash))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,6 +71,7 @@ constructor(
|
||||||
private fun fileName(perceptualHash: String) = "cover_$perceptualHash.png"
|
private fun fileName(perceptualHash: String) = "cover_$perceptualHash.png"
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
|
const val COVER_KEY_SAMPLE = 32
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
val COVER_CACHE_FORMAT =
|
val COVER_CACHE_FORMAT =
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
|
Loading…
Reference in a new issue