Fix some strict mode

This commit is contained in:
birdbird 2023-04-01 12:06:34 +00:00
parent 76d2fcdcc3
commit aa2c460529
20 changed files with 218 additions and 190 deletions

View File

@ -18,10 +18,11 @@ import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.drakeet.multitype.ItemViewDelegate import com.drakeet.multitype.ItemViewDelegate
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.Album import org.moire.ultrasonic.domain.Album
import org.moire.ultrasonic.imageloader.ImageLoader
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.util.LayoutType import org.moire.ultrasonic.util.LayoutType
import org.moire.ultrasonic.util.Settings.shouldUseId3Tags import org.moire.ultrasonic.util.Settings.shouldUseId3Tags
import timber.log.Timber import timber.log.Timber
@ -32,7 +33,6 @@ import timber.log.Timber
open class AlbumRowDelegate( open class AlbumRowDelegate(
open val onItemClick: (Album) -> Unit, open val onItemClick: (Album) -> Unit,
open val onContextMenuClick: (MenuItem, Album) -> Boolean, open val onContextMenuClick: (MenuItem, Album) -> Boolean,
private val imageLoader: ImageLoader
) : ItemViewDelegate<Album, AlbumRowDelegate.ListViewHolder>(), KoinComponent { ) : ItemViewDelegate<Album, AlbumRowDelegate.ListViewHolder>(), KoinComponent {
private val starDrawable: Int = R.drawable.ic_star_full private val starDrawable: Int = R.drawable.ic_star_full
@ -58,10 +58,13 @@ open class AlbumRowDelegate(
holder.star.setImageResource(if (item.starred) starDrawable else starHollowDrawable) holder.star.setImageResource(if (item.starred) starDrawable else starHollowDrawable)
holder.star.setOnClickListener { onStarClick(item, holder.star) } holder.star.setOnClickListener { onStarClick(item, holder.star) }
imageLoader.loadImage( val imageLoaderProvider: ImageLoaderProvider by inject()
holder.coverArt, item, imageLoaderProvider.executeOn {
false, 0, R.drawable.unknown_album it.loadImage(
) holder.coverArt, item,
false, 0, R.drawable.unknown_album
)
}
} }
/** /**
@ -148,8 +151,7 @@ open class AlbumRowDelegate(
class AlbumGridDelegate( class AlbumGridDelegate(
onItemClick: (Album) -> Unit, onItemClick: (Album) -> Unit,
onContextMenuClick: (MenuItem, Album) -> Boolean, onContextMenuClick: (MenuItem, Album) -> Boolean
imageLoader: ImageLoader ) : AlbumRowDelegate(onItemClick, onContextMenuClick) {
) : AlbumRowDelegate(onItemClick, onContextMenuClick, imageLoader) {
override var layoutType = LayoutType.COVER override var layoutType = LayoutType.COVER
} }

View File

@ -18,11 +18,12 @@ import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.drakeet.multitype.ItemViewBinder import com.drakeet.multitype.ItemViewBinder
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.ArtistOrIndex import org.moire.ultrasonic.domain.ArtistOrIndex
import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.imageloader.ImageLoader import org.moire.ultrasonic.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
@ -33,7 +34,6 @@ import org.moire.ultrasonic.util.Util
class ArtistRowBinder( class ArtistRowBinder(
val onItemClick: (ArtistOrIndex) -> Unit, val onItemClick: (ArtistOrIndex) -> Unit,
val onContextMenuClick: (MenuItem, ArtistOrIndex) -> Boolean, val onContextMenuClick: (MenuItem, ArtistOrIndex) -> Boolean,
private val imageLoader: ImageLoader,
private val enableSections: Boolean = true private val enableSections: Boolean = true
) : ItemViewBinder<ArtistOrIndex, ArtistRowBinder.ViewHolder>(), ) : ItemViewBinder<ArtistOrIndex, ArtistRowBinder.ViewHolder>(),
KoinComponent, KoinComponent,
@ -59,17 +59,21 @@ class ArtistRowBinder(
holder.coverArtId = item.coverArt holder.coverArtId = item.coverArt
val imageLoaderProvider: ImageLoaderProvider by inject()
if (showArtistPicture()) { if (showArtistPicture()) {
holder.coverArt.visibility = View.VISIBLE holder.coverArt.visibility = View.VISIBLE
val key = FileUtil.getArtistArtKey(item.name, false) val key = FileUtil.getArtistArtKey(item.name, false)
imageLoader.loadImage( imageLoaderProvider.executeOn {
view = holder.coverArt, it.loadImage(
id = holder.coverArtId, view = holder.coverArt,
key = key, id = holder.coverArtId,
large = false, key = key,
size = 0, large = false,
defaultResourceId = R.drawable.ic_contact_picture size = 0,
) defaultResourceId = R.drawable.ic_contact_picture
)
}
} else { } else {
holder.coverArt.visibility = View.GONE holder.coverArt.visibility = View.GONE
} }

View File

@ -52,10 +52,12 @@ class HeaderViewBinder(
val artworkSelection = random.nextInt(item.childCount) val artworkSelection = random.nextInt(item.childCount)
imageLoaderProvider.getImageLoader().loadImage( imageLoaderProvider.executeOn {
holder.coverArtView, item.entries[artworkSelection], false, it.loadImage(
Util.getAlbumImageSize(context) holder.coverArtView, item.entries[artworkSelection], false,
) Util.getAlbumImageSize(context)
)
}
if (item.name != null) { if (item.name != null) {
holder.titleView.isVisible = true holder.titleView.isVisible = true

View File

@ -20,7 +20,9 @@ import org.moire.ultrasonic.di.mediaPlayerModule
import org.moire.ultrasonic.di.musicServiceModule import org.moire.ultrasonic.di.musicServiceModule
import org.moire.ultrasonic.log.FileLoggerTree import org.moire.ultrasonic.log.FileLoggerTree
import org.moire.ultrasonic.log.TimberKoinLogger import org.moire.ultrasonic.log.TimberKoinLogger
import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Storage
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
import timber.log.Timber import timber.log.Timber
import timber.log.Timber.DebugTree import timber.log.Timber.DebugTree
@ -61,6 +63,9 @@ class UApp : MultiDexApplication() {
FileLoggerTree.plantToTimberForest() FileLoggerTree.plantToTimberForest()
Util.dumpSettingsToLog() Util.dumpSettingsToLog()
} }
// Populate externalFilesDir early
FileUtil.cachedUltrasonicDirectory = FileUtil.ultrasonicDirectory
Storage.mediaRoot.value
isFirstRun = Util.isFirstRun() isFirstRun = Util.isFirstRun()
} }

View File

@ -11,14 +11,12 @@ import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
import org.moire.ultrasonic.api.subsonic.SubsonicClientConfiguration import org.moire.ultrasonic.api.subsonic.SubsonicClientConfiguration
import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.imageloader.ImageLoader
import org.moire.ultrasonic.log.TimberOkHttpLogger import org.moire.ultrasonic.log.TimberOkHttpLogger
import org.moire.ultrasonic.service.CachedMusicService import org.moire.ultrasonic.service.CachedMusicService
import org.moire.ultrasonic.service.MusicService import org.moire.ultrasonic.service.MusicService
import org.moire.ultrasonic.service.OfflineMusicService import org.moire.ultrasonic.service.OfflineMusicService
import org.moire.ultrasonic.service.RESTMusicService import org.moire.ultrasonic.service.RESTMusicService
import org.moire.ultrasonic.subsonic.DownloadHandler import org.moire.ultrasonic.subsonic.DownloadHandler
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker
import org.moire.ultrasonic.subsonic.ShareHandler import org.moire.ultrasonic.subsonic.ShareHandler
import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Constants
@ -71,8 +69,6 @@ val musicServiceModule = module {
OfflineMusicService() OfflineMusicService()
} }
single { ImageLoader(androidContext(), get(), ImageLoaderProvider.config) }
single { DownloadHandler(get(), get()) } single { DownloadHandler(get(), get()) }
single { NetworkAndStorageChecker(androidContext()) } single { NetworkAndStorageChecker(androidContext()) }
single { ShareHandler(androidContext()) } single { ShareHandler(androidContext()) }

View File

@ -204,14 +204,12 @@ class AlbumListFragment(
setLayoutType(layoutType) setLayoutType(layoutType)
val imageLoader = imageLoaderProvider.getImageLoader()
// Magic to switch between different view layouts: // Magic to switch between different view layouts:
// We register two delegates, one which layouts grid items and one which layouts row items // We register two delegates, one which layouts grid items and one which layouts row items
// Based on the current status of the ViewType, the right delegate is picked. // Based on the current status of the ViewType, the right delegate is picked.
viewAdapter.register(Album::class).to( viewAdapter.register(Album::class).to(
AlbumRowDelegate(::onItemClick, ::onContextMenuItemSelected, imageLoader), AlbumRowDelegate(::onItemClick, ::onContextMenuItemSelected),
AlbumGridDelegate(::onItemClick, ::onContextMenuItemSelected, imageLoader) AlbumGridDelegate(::onItemClick, ::onContextMenuItemSelected)
).withKotlinClassLinker { _, _ -> ).withKotlinClassLinker { _, _ ->
when (layoutType) { when (layoutType) {
LayoutType.COVER -> AlbumGridDelegate::class LayoutType.COVER -> AlbumGridDelegate::class

View File

@ -53,8 +53,7 @@ class ArtistListFragment : EntryListFragment<ArtistOrIndex>() {
viewAdapter.register( viewAdapter.register(
ArtistRowBinder( ArtistRowBinder(
{ entry -> onItemClick(entry) }, { entry -> onItemClick(entry) },
{ menuItem, entry -> onContextMenuItemSelected(menuItem, entry) }, { menuItem, entry -> onContextMenuItemSelected(menuItem, entry) }
imageLoaderProvider.getImageLoader()
) )
) )
} }

View File

@ -49,7 +49,7 @@ class NowPlayingFragment : Fragment() {
private var rxBusSubscription: Disposable? = null private var rxBusSubscription: Disposable? = null
private val mediaPlayerController: MediaPlayerController by inject() private val mediaPlayerController: MediaPlayerController by inject()
private val imageLoader: ImageLoaderProvider by inject() private val imageLoaderProvider: ImageLoaderProvider by inject()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
applyTheme(this.context) applyTheme(this.context)
@ -97,12 +97,14 @@ class NowPlayingFragment : Fragment() {
val title = file.title val title = file.title
val artist = file.artist val artist = file.artist
imageLoader.getImageLoader().loadImage( imageLoaderProvider.executeOn {
nowPlayingAlbumArtImage, it.loadImage(
file, nowPlayingAlbumArtImage,
false, file,
getNotificationImageSize(requireContext()) false,
) getNotificationImageSize(requireContext())
)
}
nowPlayingTrack!!.text = title nowPlayingTrack!!.text = title
nowPlayingArtist!!.text = artist nowPlayingArtist!!.text = artist

View File

@ -1060,8 +1060,10 @@ class PlayerFragment :
downloadTrackTextView.text = trackFormat downloadTrackTextView.text = trackFormat
downloadTotalDurationTextView.text = duration downloadTotalDurationTextView.text = duration
imageLoaderProvider.getImageLoader() imageLoaderProvider.executeOn {
.loadImage(albumArtImageView, currentSong, true, 0) it.loadImage(albumArtImageView, currentSong, true, 0)
}
displaySongRating() displaySongRating()
} else { } else {
currentSong = null currentSong = null
@ -1072,8 +1074,9 @@ class PlayerFragment :
bitrateFormatTextView.text = null bitrateFormatTextView.text = null
downloadTrackTextView.text = null downloadTrackTextView.text = null
downloadTotalDurationTextView.text = null downloadTotalDurationTextView.text = null
imageLoaderProvider.getImageLoader() imageLoaderProvider.executeOn {
.loadImage(albumArtImageView, null, true, 0) it.loadImage(albumArtImageView, null, true, 0)
}
} }
} }

View File

@ -103,7 +103,6 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
ArtistRowBinder( ArtistRowBinder(
onItemClick = ::onItemClick, onItemClick = ::onItemClick,
onContextMenuClick = ::onContextMenuItemSelected, onContextMenuClick = ::onContextMenuItemSelected,
imageLoader = imageLoaderProvider.getImageLoader(),
enableSections = false enableSections = false
) )
) )
@ -111,8 +110,7 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
viewAdapter.register( viewAdapter.register(
AlbumRowDelegate( AlbumRowDelegate(
onItemClick = ::onItemClick, onItemClick = ::onItemClick,
onContextMenuClick = ::onContextMenuItemSelected, onContextMenuClick = ::onContextMenuItemSelected
imageLoader = imageLoaderProvider.getImageLoader()
) )
) )

View File

@ -146,8 +146,7 @@ open class TrackCollectionFragment(
viewAdapter.register( viewAdapter.register(
AlbumRowDelegate( AlbumRowDelegate(
{ entry -> onItemClick(entry) }, { entry -> onItemClick(entry) },
{ menuItem, entry -> onContextMenuItemSelected(menuItem, entry) }, { menuItem, entry -> onContextMenuItemSelected(menuItem, entry) }
imageLoaderProvider.getImageLoader()
) )
) )

View File

@ -1,91 +0,0 @@
package org.moire.ultrasonic.imageloader
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Build
import java.io.File
import org.moire.ultrasonic.domain.Track
import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.Util
import timber.log.Timber
@Suppress("UtilityClassWithPublicConstructor")
class BitmapUtils {
companion object {
fun getAvatarBitmapFromDisk(
username: String?,
size: Int
): Bitmap? {
if (username == null) return null
val avatarFile = FileUtil.getAvatarFile(username)
val bitmap: Bitmap? = null
if (avatarFile != null && avatarFile.exists()) {
return getBitmapFromDisk(avatarFile.path, size, bitmap)
}
return null
}
fun getAlbumArtBitmapFromDisk(
track: Track?,
size: Int
): Bitmap? {
if (track == null) return null
val albumArtFile = FileUtil.getAlbumArtFile(track)
val bitmap: Bitmap? = null
if (File(albumArtFile).exists()) {
return getBitmapFromDisk(albumArtFile, size, bitmap)
}
return null
}
fun getAlbumArtBitmapFromDisk(
filename: String,
size: Int?
): Bitmap? {
val albumArtFile = FileUtil.getAlbumArtFile(filename)
val bitmap: Bitmap? = null
if (File(albumArtFile).exists()) {
return getBitmapFromDisk(albumArtFile, size, bitmap)
}
return null
}
@Suppress("DEPRECATION")
private fun getBitmapFromDisk(
path: String,
size: Int?,
bitmap: Bitmap?
): Bitmap? {
var bitmap1 = bitmap
val opt = BitmapFactory.Options()
if (size != null && size > 0) {
// With this flag we only calculate the size first
opt.inJustDecodeBounds = true
// Decode the size
BitmapFactory.decodeFile(path, opt)
// Now set the remaining flags
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
opt.inDither = true
opt.inPreferQualityOverSpeed = true
}
opt.inSampleSize = Util.calculateInSampleSize(
opt,
size,
Util.getScaledHeight(opt.outHeight.toDouble(), opt.outWidth.toDouble(), size)
)
// Enable real decoding
opt.inJustDecodeBounds = false
}
try {
bitmap1 = BitmapFactory.decodeFile(path, opt)
} catch (expected: Exception) {
Timber.e(expected, "Exception in BitmapFactory.decodeFile()")
}
return bitmap1
}
}
}

View File

@ -1,14 +1,21 @@
package org.moire.ultrasonic.imageloader package org.moire.ultrasonic.imageloader
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Build
import com.squareup.picasso.Picasso.LoadedFrom.DISK import com.squareup.picasso.Picasso.LoadedFrom.DISK
import com.squareup.picasso.Picasso.LoadedFrom.NETWORK import com.squareup.picasso.Picasso.LoadedFrom.NETWORK
import com.squareup.picasso.Request import com.squareup.picasso.Request
import com.squareup.picasso.RequestHandler import com.squareup.picasso.RequestHandler
import java.io.File
import java.io.IOException import java.io.IOException
import okio.source import okio.source
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.FileUtil.SUFFIX_LARGE import org.moire.ultrasonic.util.FileUtil.SUFFIX_LARGE
import org.moire.ultrasonic.util.FileUtil.SUFFIX_SMALL import org.moire.ultrasonic.util.FileUtil.SUFFIX_SMALL
import org.moire.ultrasonic.util.Util
import timber.log.Timber
/** /**
* Loads cover arts from subsonic api. * Loads cover arts from subsonic api.
@ -32,9 +39,9 @@ class CoverArtRequestHandler(private val client: SubsonicAPIClient) : RequestHan
// requesting the down-sized image from the network. // requesting the down-sized image from the network.
val key = request.stableKey!! val key = request.stableKey!!
val largeKey = key.replace(SUFFIX_SMALL, SUFFIX_LARGE) val largeKey = key.replace(SUFFIX_SMALL, SUFFIX_LARGE)
var cache = BitmapUtils.getAlbumArtBitmapFromDisk(largeKey, size?.toInt()) var cache = getAlbumArtBitmapFromDisk(largeKey, size?.toInt())
if (cache == null && key != largeKey) { if (cache == null && key != largeKey) {
cache = BitmapUtils.getAlbumArtBitmapFromDisk(key, size?.toInt()) cache = getAlbumArtBitmapFromDisk(key, size?.toInt())
} }
if (cache != null) { if (cache != null) {
return Result(cache, DISK) return Result(cache, DISK)
@ -57,4 +64,54 @@ class CoverArtRequestHandler(private val client: SubsonicAPIClient) : RequestHan
// Throw an error if still not successful // Throw an error if still not successful
throw IOException("${response.apiError}") throw IOException("${response.apiError}")
} }
private fun getAlbumArtBitmapFromDisk(
filename: String,
size: Int?
): Bitmap? {
val albumArtFile = FileUtil.getAlbumArtFile(filename)
val bitmap: Bitmap? = null
if (File(albumArtFile).exists()) {
return getBitmapFromDisk(albumArtFile, size, bitmap)
}
return null
}
private fun getBitmapFromDisk(
path: String,
size: Int?,
bitmap: Bitmap?
): Bitmap? {
var bitmap1 = bitmap
val opt = BitmapFactory.Options()
if (size != null && size > 0) {
// With this flag we only calculate the size first
opt.inJustDecodeBounds = true
// Decode the size
BitmapFactory.decodeFile(path, opt)
// Now set the remaining flags
@Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
opt.inDither = true
opt.inPreferQualityOverSpeed = true
}
opt.inSampleSize = Util.calculateInSampleSize(
opt,
size,
Util.getScaledHeight(opt.outHeight.toDouble(), opt.outWidth.toDouble(), size)
)
// Enable real decoding
opt.inJustDecodeBounds = false
}
try {
bitmap1 = BitmapFactory.decodeFile(path, opt)
} catch (expected: Exception) {
Timber.e(expected, "Exception in BitmapFactory.decodeFile()")
}
return bitmap1
}
} }

View File

@ -16,6 +16,8 @@ import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
import org.moire.ultrasonic.api.subsonic.throwOnFailure import org.moire.ultrasonic.api.subsonic.throwOnFailure
@ -33,8 +35,8 @@ import timber.log.Timber
class ImageLoader( class ImageLoader(
context: Context, context: Context,
apiClient: SubsonicAPIClient, apiClient: SubsonicAPIClient,
private val config: ImageLoaderConfig private val config: ImageLoaderConfig,
) { ) : CoroutineScope by CoroutineScope(Dispatchers.IO) {
private val cacheInProgress: ConcurrentHashMap<String, CountDownLatch> = ConcurrentHashMap() private val cacheInProgress: ConcurrentHashMap<String, CountDownLatch> = ConcurrentHashMap()
// Shortcut // Shortcut
@ -126,6 +128,7 @@ class ImageLoader(
defaultResourceId: Int = R.drawable.unknown_album defaultResourceId: Int = R.drawable.unknown_album
) { ) {
val id = entry?.coverArt val id = entry?.coverArt
// TODO getAlbumArtKey() accesses the disk from the UI thread..
val key = FileUtil.getAlbumArtKey(entry, large) val key = FileUtil.getAlbumArtKey(entry, large)
loadImage(view, id, key, large, size, defaultResourceId) loadImage(view, id, key, large, size, defaultResourceId)

View File

@ -2,9 +2,16 @@ package org.moire.ultrasonic.log
import java.io.File import java.io.File
import java.io.FileWriter import java.io.FileWriter
import java.io.PrintWriter
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ObsoleteCoroutinesApi
import kotlinx.coroutines.channels.actor
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.Util.safeClose import org.moire.ultrasonic.util.Util.safeClose
import timber.log.Timber import timber.log.Timber
@ -14,33 +21,58 @@ import timber.log.Timber
* Subclass of the DebugTree so it inherits the Tag handling * Subclass of the DebugTree so it inherits the Tag handling
*/ */
@Suppress("MagicNumber") @Suppress("MagicNumber")
class FileLoggerTree : Timber.DebugTree() { class FileLoggerTree : Timber.DebugTree(), CoroutineScope by CoroutineScope(Dispatchers.IO) {
private val dateFormat = SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault()) private val dateFormat = SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault())
@OptIn(ObsoleteCoroutinesApi::class)
private val logMessageActor = actor<LogMessage> {
for (msg in channel)
writeLogToFile(msg.file, msg.priority, msg.tag, msg.message, msg.t)
}
data class LogMessage(
val file: File?,
val priority: Int,
val tag: String?,
val message: String,
val t: Throwable?
)
/** /**
* Writes a log entry to file * Writes a log entry to file
* * This methods sends the log entry to the coroutine actor, which then processes the entries
* TODO: This seems to be writing in the main thread. Should be done in background... * in FIFO order on an IO-Thread
*/ */
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
var writer: FileWriter? = null launch {
callNum++
try {
getNextLogFile() getNextLogFile()
writer = FileWriter(file, true) logMessageActor.send(
val exceptionString = t?.toString() ?: "" LogMessage(file, priority, tag, message, t)
val time: String = dateFormat.format(Date()) )
synchronized(file!!) { }
writer.write( }
"$time: ${logLevelToString(priority)} $tag $message $exceptionString\n"
) private suspend fun writeLogToFile(
writer.flush() file: File?,
priority: Int,
tag: String?,
message: String,
t: Throwable?
) {
val time: String = dateFormat.format(Date())
val exceptionString = t?.toString() ?: ""
withContext(Dispatchers.IO) {
var pw: PrintWriter? = null
try {
pw = PrintWriter(FileWriter(file, true))
pw.println("$time: ${logLevelToString(priority)} $tag $message $exceptionString\n")
t?.printStackTrace(pw)
} catch (all: Throwable) {
// Using base class DebugTree here, we don't want to try to log this into file
super.log(6, TAG, String.format("Failed to write log to %s", file), all)
} finally {
pw?.safeClose()
} }
} catch (all: Throwable) {
// Using base class DebugTree here, we don't want to try to log this into file
super.log(6, TAG, String.format("Failed to write log to %s", file), all)
} finally {
writer.safeClose()
} }
} }

View File

@ -19,13 +19,13 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.domain.Track
import org.moire.ultrasonic.imageloader.ImageLoader import org.moire.ultrasonic.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.FileUtil
import timber.log.Timber import timber.log.Timber
class AlbumArtContentProvider : ContentProvider(), KoinComponent { class AlbumArtContentProvider : ContentProvider(), KoinComponent {
private val imageLoader: ImageLoader by inject() private val imageLoaderProvider: ImageLoaderProvider by inject()
companion object { companion object {
fun mapArtworkToContentProviderUri(track: Track?): Uri? { fun mapArtworkToContentProviderUri(track: Track?): Uri? {
@ -56,7 +56,9 @@ class AlbumArtContentProvider : ContentProvider(), KoinComponent {
val albumArtFile = FileUtil.getAlbumArtFile(parts[1]) val albumArtFile = FileUtil.getAlbumArtFile(parts[1])
Timber.d("AlbumArtContentProvider openFile id: %s; file: %s", parts[0], albumArtFile) Timber.d("AlbumArtContentProvider openFile id: %s; file: %s", parts[0], albumArtFile)
imageLoader.cacheCoverArt(parts[0], albumArtFile) imageLoaderProvider.executeOn {
it.cacheCoverArt(parts[0], albumArtFile)
}
val file = File(albumArtFile) val file = File(albumArtFile)
if (!file.exists()) return null if (!file.exists()) return null

View File

@ -23,7 +23,6 @@ import android.widget.RemoteViews
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.activity.NavigationActivity import org.moire.ultrasonic.activity.NavigationActivity
import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.domain.Track
import org.moire.ultrasonic.imageloader.BitmapUtils
import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver import org.moire.ultrasonic.receiver.MediaButtonIntentReceiver
import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Constants
import timber.log.Timber import timber.log.Timber
@ -200,17 +199,8 @@ open class UltrasonicAppWidgetProvider : AppWidgetProvider() {
} }
// Set the cover art // Set the cover art
try { try {
val bitmap = val uri = AlbumArtContentProvider.mapArtworkToContentProviderUri(currentSong)
if (currentSong == null) null else BitmapUtils.getAlbumArtBitmapFromDisk( views.setImageViewUri(R.id.appwidget_coverart, uri)
currentSong,
240
)
if (bitmap == null) {
// Set default cover art
views.setImageViewResource(R.id.appwidget_coverart, R.drawable.unknown_album)
} else {
views.setImageViewBitmap(R.id.appwidget_coverart, bitmap)
}
} catch (all: Exception) { } catch (all: Exception) {
Timber.e(all, "Failed to load cover art") Timber.e(all, "Failed to load cover art")
views.setImageViewResource(R.id.appwidget_coverart, R.drawable.unknown_album) views.setImageViewResource(R.id.appwidget_coverart, R.drawable.unknown_album)

View File

@ -257,12 +257,12 @@ class DownloadTask(
offlineDB.trackDao().insert(this) offlineDB.trackDao().insert(this)
// Download the largest size that we can display in the UI // Download the largest size that we can display in the UI
val imageLoader = imageLoaderProvider.getImageLoader() imageLoaderProvider.executeOn { imageLoader ->
imageLoader.cacheCoverArt(this) imageLoader.cacheCoverArt(this)
// Cache small copies of the Artist picture
// Cache small copies of the Artist picture directArtist?.let { imageLoader.cacheArtistPicture(it) }
directArtist?.let { imageLoader.cacheArtistPicture(it) } compilationArtist?.let { imageLoader.cacheArtistPicture(it) }
compilationArtist?.let { imageLoader.cacheArtistPicture(it) } }
} }
private fun cacheArtist( private fun cacheArtist(

View File

@ -5,6 +5,7 @@ import androidx.core.content.res.ResourcesCompat
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.get import org.koin.core.component.get
import org.koin.core.qualifier.named import org.koin.core.qualifier.named
@ -14,11 +15,13 @@ import org.moire.ultrasonic.imageloader.ImageLoader
import org.moire.ultrasonic.imageloader.ImageLoaderConfig import org.moire.ultrasonic.imageloader.ImageLoaderConfig
import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
import timber.log.Timber
/** /**
* Handles the lifetime of the Image Loader * Handles the lifetime of the Image Loader
*/ */
class ImageLoaderProvider(val context: Context) : class
ImageLoaderProvider(val context: Context) :
KoinComponent, KoinComponent,
CoroutineScope by CoroutineScope(Dispatchers.IO) { CoroutineScope by CoroutineScope(Dispatchers.IO) {
private var imageLoader: ImageLoader? = null private var imageLoader: ImageLoader? = null
@ -29,22 +32,39 @@ class ImageLoaderProvider(val context: Context) :
imageLoader = null imageLoader = null
} }
init {
Timber.e("Prepping Loader")
// Populate the ImageLoader async & early
launch {
getImageLoader()
}
}
@Synchronized @Synchronized
fun getImageLoader(): ImageLoader { fun getImageLoader(): ImageLoader {
// We need to generate a new ImageLoader if the server has changed... // We need to generate a new ImageLoader if the server has changed...
val currentID = get<String>(named("ServerID")) val currentID = get<String>(named("ServerID"))
if (imageLoader == null || currentID != serverID) { if (imageLoader == null || currentID != serverID) {
imageLoader = get() imageLoader = ImageLoader(UApp.applicationContext(), get(), config)
serverID = currentID serverID = currentID
}
launch { launch {
FileUtil.ensureAlbumArtDirectory() FileUtil.ensureAlbumArtDirectory()
}
} }
return imageLoader!! return imageLoader!!
} }
fun executeOn(cb: (iL: ImageLoader) -> Unit) {
launch {
val iL = getImageLoader()
withContext(Dispatchers.Main) {
cb(iL)
}
}
}
companion object { companion object {
val config by lazy { val config by lazy {
var defaultSize = 0 var defaultSize = 0

View File

@ -249,17 +249,24 @@ object FileUtil {
return dir return dir
} }
var cachedUltrasonicDirectory: File? = null
// After Android M, the location of the files must be queried differently. // After Android M, the location of the files must be queried differently.
// GetExternalFilesDir will always return a directory which Ultrasonic // GetExternalFilesDir will always return a directory which Ultrasonic
// can access without any extra privileges. // can access without any extra privileges.
@JvmStatic @JvmStatic
val ultrasonicDirectory: File val ultrasonicDirectory: File
get() { get() {
// Return cached if possible
if (cachedUltrasonicDirectory != null) return cachedUltrasonicDirectory!!
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) File( cachedUltrasonicDirectory = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) File(
Environment.getExternalStorageDirectory(), Environment.getExternalStorageDirectory(),
"Android/data/org.moire.ultrasonic" "Android/data/org.moire.ultrasonic"
) else UApp.applicationContext().getExternalFilesDir(null)!! ) else UApp.applicationContext().getExternalFilesDir(null)!!
return cachedUltrasonicDirectory!!
} }
@JvmStatic @JvmStatic