kopia lustrzana https://dev.funkwhale.audio/funkwhale/funkwhale-android
Manage cached and downloaded tracks separately. Downloaded track are not automatically evicted and do not count towards cache storage limit. Contributes to #37. Fixed an issue where the event bus on main would be duplicated.
rodzic
2eff3263d2
commit
e539cc26dd
|
@ -3,16 +3,14 @@ package com.github.apognu.otter
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import com.github.apognu.otter.playback.QueueManager
|
import com.github.apognu.otter.playback.QueueManager
|
||||||
import com.github.apognu.otter.utils.Cache
|
import com.github.apognu.otter.utils.*
|
||||||
import com.github.apognu.otter.utils.Command
|
|
||||||
import com.github.apognu.otter.utils.Event
|
|
||||||
import com.github.apognu.otter.utils.Request
|
|
||||||
import com.google.android.exoplayer2.database.ExoDatabaseProvider
|
import com.google.android.exoplayer2.database.ExoDatabaseProvider
|
||||||
import com.google.android.exoplayer2.offline.DefaultDownloadIndex
|
import com.google.android.exoplayer2.offline.DefaultDownloadIndex
|
||||||
import com.google.android.exoplayer2.offline.DefaultDownloaderFactory
|
import com.google.android.exoplayer2.offline.DefaultDownloaderFactory
|
||||||
import com.google.android.exoplayer2.offline.DownloadManager
|
import com.google.android.exoplayer2.offline.DownloadManager
|
||||||
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper
|
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper
|
||||||
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor
|
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor
|
||||||
|
import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor
|
||||||
import com.google.android.exoplayer2.upstream.cache.SimpleCache
|
import com.google.android.exoplayer2.upstream.cache.SimpleCache
|
||||||
import com.preference.PowerPreference
|
import com.preference.PowerPreference
|
||||||
import kotlinx.coroutines.channels.BroadcastChannel
|
import kotlinx.coroutines.channels.BroadcastChannel
|
||||||
|
@ -36,6 +34,7 @@ class Otter : Application() {
|
||||||
val progressBus: BroadcastChannel<Triple<Int, Int, Int>> = ConflatedBroadcastChannel()
|
val progressBus: BroadcastChannel<Triple<Int, Int, Int>> = ConflatedBroadcastChannel()
|
||||||
|
|
||||||
private val exoDatabase: ExoDatabaseProvider by lazy { ExoDatabaseProvider(this) }
|
private val exoDatabase: ExoDatabaseProvider by lazy { ExoDatabaseProvider(this) }
|
||||||
|
|
||||||
val exoCache: SimpleCache by lazy {
|
val exoCache: SimpleCache by lazy {
|
||||||
PowerPreference.getDefaultFile().getInt("media_cache_size", 1).toLong().let {
|
PowerPreference.getDefaultFile().getInt("media_cache_size", 1).toLong().let {
|
||||||
SimpleCache(
|
SimpleCache(
|
||||||
|
@ -45,8 +44,17 @@ class Otter : Application() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val exoDownloadCache: SimpleCache by lazy {
|
||||||
|
SimpleCache(
|
||||||
|
cacheDir.resolve("downloads"),
|
||||||
|
NoOpCacheEvictor(),
|
||||||
|
exoDatabase
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val exoDownloadManager: DownloadManager by lazy {
|
val exoDownloadManager: DownloadManager by lazy {
|
||||||
DownloaderConstructorHelper(exoCache, QueueManager.factory(this)).run {
|
DownloaderConstructorHelper(exoDownloadCache, QueueManager.factory(this)).run {
|
||||||
DownloadManager(this@Otter, DefaultDownloadIndex(exoDatabase), DefaultDownloaderFactory(this))
|
DownloadManager(this@Otter, DefaultDownloadIndex(exoDatabase), DefaultDownloaderFactory(this))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,9 +41,11 @@ import kotlinx.android.synthetic.main.partial_now_playing.*
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.Dispatchers.Main
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
enum class ResultCode(val code: Int) {
|
enum class ResultCode(val code: Int) {
|
||||||
|
@ -53,6 +55,8 @@ class MainActivity : AppCompatActivity() {
|
||||||
private val favoriteRepository = FavoritesRepository(this)
|
private val favoriteRepository = FavoritesRepository(this)
|
||||||
private val favoriteCheckRepository = FavoritedRepository(this)
|
private val favoriteCheckRepository = FavoritedRepository(this)
|
||||||
|
|
||||||
|
private var bus: Job? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
@ -69,10 +73,6 @@ class MainActivity : AppCompatActivity() {
|
||||||
.beginTransaction()
|
.beginTransaction()
|
||||||
.replace(R.id.container, BrowseFragment())
|
.replace(R.id.container, BrowseFragment())
|
||||||
.commit()
|
.commit()
|
||||||
|
|
||||||
watchEventBus()
|
|
||||||
|
|
||||||
CommandBus.send(Command.RefreshService)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
@ -116,6 +116,19 @@ class MainActivity : AppCompatActivity() {
|
||||||
landscape_queue?.let {
|
landscape_queue?.let {
|
||||||
supportFragmentManager.beginTransaction().replace(R.id.landscape_queue, LandscapeQueueFragment()).commit()
|
supportFragmentManager.beginTransaction().replace(R.id.landscape_queue, LandscapeQueueFragment()).commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (bus == null) {
|
||||||
|
watchEventBus()
|
||||||
|
}
|
||||||
|
|
||||||
|
CommandBus.send(Command.RefreshService)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
|
||||||
|
bus?.cancel()
|
||||||
|
bus = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
|
@ -212,7 +225,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
@SuppressLint("NewApi")
|
||||||
private fun watchEventBus() {
|
private fun watchEventBus() {
|
||||||
GlobalScope.launch(Main) {
|
bus = GlobalScope.launch(Main) {
|
||||||
EventBus.get().collect { message ->
|
EventBus.get().collect { message ->
|
||||||
when (message) {
|
when (message) {
|
||||||
is Event.LogOut -> {
|
is Event.LogOut -> {
|
||||||
|
|
|
@ -2,6 +2,8 @@ package com.github.apognu.otter.adapters
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.PorterDuff
|
||||||
|
import android.graphics.PorterDuffColorFilter
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
|
@ -74,11 +76,23 @@ class FavoritesAdapter(private val context: Context?, private val favoriteListen
|
||||||
false -> holder.favorite.setColorFilter(context.getColor(R.color.colorSelected))
|
false -> holder.favorite.setColorFilter(context.getColor(R.color.colorSelected))
|
||||||
}
|
}
|
||||||
|
|
||||||
when (favorite.downloaded) {
|
when (favorite.cached || favorite.downloaded) {
|
||||||
true -> holder.title.setCompoundDrawablesWithIntrinsicBounds(R.drawable.downloaded, 0, 0, 0)
|
true -> holder.title.setCompoundDrawablesWithIntrinsicBounds(R.drawable.downloaded, 0, 0, 0)
|
||||||
false -> holder.title.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
|
false -> holder.title.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (favorite.cached && !favorite.downloaded) {
|
||||||
|
holder.title.compoundDrawables.forEach {
|
||||||
|
it?.colorFilter = PorterDuffColorFilter(context.getColor(R.color.cached), PorterDuff.Mode.SRC_IN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (favorite.downloaded) {
|
||||||
|
holder.title.compoundDrawables.forEach {
|
||||||
|
it?.colorFilter = PorterDuffColorFilter(context.getColor(R.color.downloaded), PorterDuff.Mode.SRC_IN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
holder.favorite.setOnClickListener {
|
holder.favorite.setOnClickListener {
|
||||||
favoriteListener.onToggleFavorite(favorite.id, !favorite.favorite)
|
favoriteListener.onToggleFavorite(favorite.id, !favorite.favorite)
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,7 @@ package com.github.apognu.otter.adapters
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
import android.graphics.*
|
||||||
import android.graphics.Typeface
|
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.view.*
|
import android.view.*
|
||||||
|
@ -95,10 +94,22 @@ class TracksAdapter(private val context: Context?, private val favoriteListener:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
when (track.downloaded) {
|
when (track.cached || track.downloaded) {
|
||||||
true -> holder.title.setCompoundDrawablesWithIntrinsicBounds(R.drawable.downloaded, 0, 0, 0)
|
true -> holder.title.setCompoundDrawablesWithIntrinsicBounds(R.drawable.downloaded, 0, 0, 0)
|
||||||
false -> holder.title.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
|
false -> holder.title.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (track.cached && !track.downloaded) {
|
||||||
|
holder.title.compoundDrawables.forEach {
|
||||||
|
it?.colorFilter = PorterDuffColorFilter(context.getColor(R.color.cached), PorterDuff.Mode.SRC_IN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (track.downloaded) {
|
||||||
|
holder.title.compoundDrawables.forEach {
|
||||||
|
it?.colorFilter = PorterDuffColorFilter(context.getColor(R.color.downloaded), PorterDuff.Mode.SRC_IN)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.actions.setOnClickListener {
|
holder.actions.setOnClickListener {
|
||||||
|
|
|
@ -9,6 +9,8 @@ import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource
|
import com.google.android.exoplayer2.source.ConcatenatingMediaSource
|
||||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
||||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory
|
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory
|
||||||
|
import com.google.android.exoplayer2.upstream.FileDataSource
|
||||||
|
import com.google.android.exoplayer2.upstream.cache.CacheDataSource
|
||||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory
|
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory
|
||||||
import com.google.android.exoplayer2.util.Util
|
import com.google.android.exoplayer2.util.Util
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
|
@ -28,7 +30,16 @@ class QueueManager(val context: Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return CacheDataSourceFactory(Otter.get().exoCache, http)
|
val playbackCache = CacheDataSourceFactory(Otter.get().exoCache, http)
|
||||||
|
|
||||||
|
return CacheDataSourceFactory(
|
||||||
|
Otter.get().exoDownloadCache,
|
||||||
|
playbackCache,
|
||||||
|
FileDataSource.Factory(),
|
||||||
|
null,
|
||||||
|
CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,
|
||||||
|
null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.github.apognu.otter.repositories
|
package com.github.apognu.otter.repositories
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import com.github.apognu.otter.Otter
|
||||||
import com.github.apognu.otter.utils.*
|
import com.github.apognu.otter.utils.*
|
||||||
import com.github.kittinunf.fuel.Fuel
|
import com.github.kittinunf.fuel.Fuel
|
||||||
import com.github.kittinunf.fuel.coroutines.awaitByteArrayResponseResult
|
import com.github.kittinunf.fuel.coroutines.awaitByteArrayResponseResult
|
||||||
|
@ -26,6 +27,13 @@ class FavoritesRepository(override val context: Context?) : Repository<Track, Tr
|
||||||
data.map { track ->
|
data.map { track ->
|
||||||
track.favorite = true
|
track.favorite = true
|
||||||
track.downloaded = downloaded.contains(track.id)
|
track.downloaded = downloaded.contains(track.id)
|
||||||
|
|
||||||
|
track.bestUpload()?.let { upload ->
|
||||||
|
val url = mustNormalizeUrl(upload.listen_url)
|
||||||
|
|
||||||
|
track.cached = Otter.get().exoCache.isCached(url, 0, upload.duration * 1000L)
|
||||||
|
}
|
||||||
|
|
||||||
track
|
track
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.github.apognu.otter.repositories
|
package com.github.apognu.otter.repositories
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import com.github.apognu.otter.Otter
|
||||||
import com.github.apognu.otter.utils.*
|
import com.github.apognu.otter.utils.*
|
||||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||||
import com.google.android.exoplayer2.offline.Download
|
import com.google.android.exoplayer2.offline.Download
|
||||||
|
@ -48,6 +49,13 @@ class TracksRepository(override val context: Context?, albumId: Int) : Repositor
|
||||||
data.map { track ->
|
data.map { track ->
|
||||||
track.favorite = favorites.contains(track.id)
|
track.favorite = favorites.contains(track.id)
|
||||||
track.downloaded = downloaded.contains(track.id)
|
track.downloaded = downloaded.contains(track.id)
|
||||||
|
|
||||||
|
track.bestUpload()?.let { upload ->
|
||||||
|
val url = mustNormalizeUrl(upload.listen_url)
|
||||||
|
|
||||||
|
track.cached = Otter.get().exoCache.isCached(url, 0, upload.duration * 1000L)
|
||||||
|
}
|
||||||
|
|
||||||
track
|
track
|
||||||
}.sortedBy { it.position }
|
}.sortedBy { it.position }
|
||||||
}
|
}
|
||||||
|
|
|
@ -100,6 +100,7 @@ data class Track(
|
||||||
) : SearchResult {
|
) : SearchResult {
|
||||||
var current: Boolean = false
|
var current: Boolean = false
|
||||||
var favorite: Boolean = false
|
var favorite: Boolean = false
|
||||||
|
var cached: Boolean = false
|
||||||
var downloaded: Boolean = false
|
var downloaded: Boolean = false
|
||||||
|
|
||||||
data class Upload(
|
data class Upload(
|
||||||
|
|
|
@ -16,4 +16,7 @@
|
||||||
|
|
||||||
<color name="whiteWhileLight">#000000</color>
|
<color name="whiteWhileLight">#000000</color>
|
||||||
<color name="blackWhileLight">#ffffff</color>
|
<color name="blackWhileLight">#ffffff</color>
|
||||||
|
|
||||||
|
<color name="downloaded">@color/controlColor</color>
|
||||||
|
<color name="cached">#aeaeae</color>
|
||||||
</resources>
|
</resources>
|
|
@ -18,4 +18,7 @@
|
||||||
|
|
||||||
<color name="whiteWhileLight">#ffffff</color>
|
<color name="whiteWhileLight">#ffffff</color>
|
||||||
<color name="blackWhileLight">#000000</color>
|
<color name="blackWhileLight">#000000</color>
|
||||||
|
|
||||||
|
<color name="downloaded">@color/colorPrimary</color>
|
||||||
|
<color name="cached">#999999</color>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Ładowanie…
Reference in New Issue