mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-06-04 01:31:03 +03:00
Fix some strict mode
This commit is contained in:
parent
76d2fcdcc3
commit
aa2c460529
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()) }
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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()
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user