#7: Add Koin as dependency injection library
This commit is contained in:
parent
36771dcafb
commit
a9319b88b0
|
@ -171,6 +171,10 @@ dependencies {
|
||||||
implementation("com.google.android.exoplayer:exoplayer-ui:${Versions.exoPlayer}")
|
implementation("com.google.android.exoplayer:exoplayer-ui:${Versions.exoPlayer}")
|
||||||
implementation("com.google.android.exoplayer:extension-mediasession:${Versions.exoPlayer}")
|
implementation("com.google.android.exoplayer:extension-mediasession:${Versions.exoPlayer}")
|
||||||
|
|
||||||
|
implementation("io.insert-koin:koin-core:${Versions.koin}")
|
||||||
|
implementation("io.insert-koin:koin-android:${Versions.koin}")
|
||||||
|
testImplementation("io.insert-koin:koin-test:${Versions.koin}")
|
||||||
|
|
||||||
implementation("com.github.PaulWoitaschek.ExoPlayer-Extensions:extension-opus:${Versions.exoPlayerExtensions}") {
|
implementation("com.github.PaulWoitaschek.ExoPlayer-Extensions:extension-opus:${Versions.exoPlayerExtensions}") {
|
||||||
isTransitive = false
|
isTransitive = false
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,19 @@
|
||||||
package audio.funkwhale.ffa
|
package audio.funkwhale.ffa
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import audio.funkwhale.ffa.playback.MediaSession
|
import audio.funkwhale.ffa.koin.ffaModule
|
||||||
import audio.funkwhale.ffa.playback.QueueManager
|
|
||||||
import audio.funkwhale.ffa.utils.*
|
import audio.funkwhale.ffa.utils.*
|
||||||
import com.google.android.exoplayer2.database.ExoDatabaseProvider
|
|
||||||
import com.google.android.exoplayer2.offline.DefaultDownloadIndex
|
|
||||||
import com.google.android.exoplayer2.offline.DefaultDownloaderFactory
|
|
||||||
import com.google.android.exoplayer2.offline.DownloadManager
|
|
||||||
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper
|
|
||||||
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.preference.PowerPreference
|
import com.preference.PowerPreference
|
||||||
import kotlinx.coroutines.channels.BroadcastChannel
|
import kotlinx.coroutines.channels.BroadcastChannel
|
||||||
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
|
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class FFA : Application() {
|
class FFA : Application() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private var instance: FFA = FFA()
|
private var instance: FFA = FFA()
|
||||||
|
|
||||||
|
@ -33,39 +27,13 @@ class FFA : Application() {
|
||||||
val requestBus: BroadcastChannel<Request> = BroadcastChannel(10)
|
val requestBus: BroadcastChannel<Request> = BroadcastChannel(10)
|
||||||
val progressBus: BroadcastChannel<Triple<Int, Int, Int>> = ConflatedBroadcastChannel()
|
val progressBus: BroadcastChannel<Triple<Int, Int, Int>> = ConflatedBroadcastChannel()
|
||||||
|
|
||||||
private val exoDatabase: ExoDatabaseProvider by lazy { ExoDatabaseProvider(this) }
|
|
||||||
|
|
||||||
val exoCache: SimpleCache by lazy {
|
|
||||||
PowerPreference.getDefaultFile().getInt("media_cache_size", 1).toLong().let {
|
|
||||||
val cacheSize = if (it == 0L) 0 else it * 1024 * 1024 * 1024
|
|
||||||
|
|
||||||
SimpleCache(
|
|
||||||
cacheDir.resolve("media"),
|
|
||||||
LeastRecentlyUsedCacheEvictor(cacheSize),
|
|
||||||
exoDatabase
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val exoDownloadCache: SimpleCache by lazy {
|
|
||||||
SimpleCache(
|
|
||||||
cacheDir.resolve("downloads"),
|
|
||||||
NoOpCacheEvictor(),
|
|
||||||
exoDatabase
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val exoDownloadManager: DownloadManager by lazy {
|
|
||||||
DownloaderConstructorHelper(exoDownloadCache, QueueManager.factory(this)).run {
|
|
||||||
DownloadManager(this@FFA, DefaultDownloadIndex(exoDatabase), DefaultDownloaderFactory(this))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val mediaSession = MediaSession(this)
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
|
startKoin {
|
||||||
|
modules(ffaModule(this@FFA))
|
||||||
|
}
|
||||||
|
|
||||||
defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
|
defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
|
||||||
|
|
||||||
Thread.setDefaultUncaughtExceptionHandler(CrashReportHandler())
|
Thread.setDefaultUncaughtExceptionHandler(CrashReportHandler())
|
||||||
|
@ -81,14 +49,13 @@ class FFA : Application() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteAllData() {
|
fun deleteAllData(context: Context) {
|
||||||
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).clear()
|
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).clear()
|
||||||
|
|
||||||
cacheDir.listFiles()?.forEach {
|
context.cacheDir.listFiles()?.forEach {
|
||||||
it.delete()
|
it.delete()
|
||||||
}
|
}
|
||||||
|
context.cacheDir.resolve("picasso-cache").deleteRecursively()
|
||||||
cacheDir.resolve("picasso-cache").deleteRecursively()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class CrashReportHandler : Thread.UncaughtExceptionHandler {
|
inner class CrashReportHandler : Thread.UncaughtExceptionHandler {
|
||||||
|
@ -107,7 +74,7 @@ class FFA : Application() {
|
||||||
|
|
||||||
builder.appendLine(e.toString())
|
builder.appendLine(e.toString())
|
||||||
|
|
||||||
Cache.set(this@FFA, "crashdump", builder.toString().toByteArray())
|
FFACache.set(this@FFA, "crashdump", builder.toString().toByteArray())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,24 +4,26 @@ import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import audio.funkwhale.ffa.FFA
|
|
||||||
import audio.funkwhale.ffa.adapters.DownloadsAdapter
|
import audio.funkwhale.ffa.adapters.DownloadsAdapter
|
||||||
import audio.funkwhale.ffa.databinding.ActivityDownloadsBinding
|
import audio.funkwhale.ffa.databinding.ActivityDownloadsBinding
|
||||||
import audio.funkwhale.ffa.utils.Event
|
import audio.funkwhale.ffa.utils.Event
|
||||||
import audio.funkwhale.ffa.utils.EventBus
|
import audio.funkwhale.ffa.utils.EventBus
|
||||||
import audio.funkwhale.ffa.utils.getMetadata
|
import audio.funkwhale.ffa.utils.getMetadata
|
||||||
import com.google.android.exoplayer2.offline.Download
|
import com.google.android.exoplayer2.offline.Download
|
||||||
|
import com.google.android.exoplayer2.offline.DownloadManager
|
||||||
import kotlinx.coroutines.Dispatchers.Default
|
import kotlinx.coroutines.Dispatchers.Default
|
||||||
import kotlinx.coroutines.Dispatchers.Main
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
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 org.koin.java.KoinJavaComponent.inject
|
||||||
|
|
||||||
class DownloadsActivity : AppCompatActivity() {
|
class DownloadsActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private lateinit var adapter: DownloadsAdapter
|
private lateinit var adapter: DownloadsAdapter
|
||||||
private lateinit var binding: ActivityDownloadsBinding
|
private lateinit var binding: ActivityDownloadsBinding
|
||||||
|
private val exoDownloadManager: DownloadManager by inject(DownloadManager::class.java)
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -63,7 +65,7 @@ class DownloadsActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private fun refresh() {
|
private fun refresh() {
|
||||||
lifecycleScope.launch(Main) {
|
lifecycleScope.launch(Main) {
|
||||||
val cursor = FFA.get().exoDownloadManager.downloadIndex.getDownloads()
|
val cursor = exoDownloadManager.downloadIndex.getDownloads()
|
||||||
|
|
||||||
adapter.downloads.clear()
|
adapter.downloads.clear()
|
||||||
|
|
||||||
|
@ -99,7 +101,7 @@ class DownloadsActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun refreshProgress() {
|
private suspend fun refreshProgress() {
|
||||||
val cursor = FFA.get().exoDownloadManager.downloadIndex.getDownloads()
|
val cursor = exoDownloadManager.downloadIndex.getDownloads()
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
val download = cursor.download
|
val download = cursor.download
|
||||||
|
|
|
@ -13,7 +13,6 @@ import audio.funkwhale.ffa.databinding.ActivityLoginBinding
|
||||||
import audio.funkwhale.ffa.fragments.LoginDialog
|
import audio.funkwhale.ffa.fragments.LoginDialog
|
||||||
import audio.funkwhale.ffa.utils.AppContext
|
import audio.funkwhale.ffa.utils.AppContext
|
||||||
import audio.funkwhale.ffa.utils.OAuth
|
import audio.funkwhale.ffa.utils.OAuth
|
||||||
import audio.funkwhale.ffa.utils.OAuthFactory
|
|
||||||
import audio.funkwhale.ffa.utils.Userinfo
|
import audio.funkwhale.ffa.utils.Userinfo
|
||||||
import audio.funkwhale.ffa.utils.log
|
import audio.funkwhale.ffa.utils.log
|
||||||
import com.github.kittinunf.fuel.Fuel
|
import com.github.kittinunf.fuel.Fuel
|
||||||
|
@ -23,19 +22,19 @@ import com.github.kittinunf.result.Result
|
||||||
import com.preference.PowerPreference
|
import com.preference.PowerPreference
|
||||||
import kotlinx.coroutines.Dispatchers.Main
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.java.KoinJavaComponent.inject
|
||||||
|
|
||||||
data class FwCredentials(val token: String, val non_field_errors: List<String>?)
|
data class FwCredentials(val token: String, val non_field_errors: List<String>?)
|
||||||
|
|
||||||
class LoginActivity : AppCompatActivity() {
|
class LoginActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private lateinit var binding: ActivityLoginBinding
|
private lateinit var binding: ActivityLoginBinding
|
||||||
private lateinit var oAuth: OAuth
|
private val oAuth: OAuth by inject(OAuth::class.java)
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
binding = ActivityLoginBinding.inflate(layoutInflater)
|
binding = ActivityLoginBinding.inflate(layoutInflater)
|
||||||
oAuth = OAuthFactory.instance()
|
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
limitContainerWidth()
|
limitContainerWidth()
|
||||||
}
|
}
|
||||||
|
@ -53,7 +52,7 @@ class LoginActivity : AppCompatActivity() {
|
||||||
.setBoolean("anonymous", false)
|
.setBoolean("anonymous", false)
|
||||||
|
|
||||||
lifecycleScope.launch(Main) {
|
lifecycleScope.launch(Main) {
|
||||||
Userinfo.get(this@LoginActivity)?.let {
|
Userinfo.get(this@LoginActivity, oAuth)?.let {
|
||||||
startActivity(Intent(this@LoginActivity, MainActivity::class.java))
|
startActivity(Intent(this@LoginActivity, MainActivity::class.java))
|
||||||
|
|
||||||
return@launch finish()
|
return@launch finish()
|
||||||
|
|
|
@ -47,6 +47,7 @@ import kotlinx.coroutines.Dispatchers.Main
|
||||||
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 org.koin.java.KoinJavaComponent.inject
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
enum class ResultCode(val code: Int) {
|
enum class ResultCode(val code: Int) {
|
||||||
|
@ -58,6 +59,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
private var menu: Menu? = null
|
private var menu: Menu? = null
|
||||||
|
|
||||||
private lateinit var binding: ActivityMainBinding
|
private lateinit var binding: ActivityMainBinding
|
||||||
|
private val oAuth: OAuth by inject(OAuth::class.java)
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -103,7 +105,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
CommandBus.send(Command.RefreshService)
|
CommandBus.send(Command.RefreshService)
|
||||||
|
|
||||||
lifecycleScope.launch(IO) {
|
lifecycleScope.launch(IO) {
|
||||||
Userinfo.get(this@MainActivity)
|
Userinfo.get(this@MainActivity, oAuth)
|
||||||
}
|
}
|
||||||
|
|
||||||
with(binding) {
|
with(binding) {
|
||||||
|
@ -260,7 +262,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
if (resultCode == ResultCode.LOGOUT.code) {
|
if (resultCode == ResultCode.LOGOUT.code) {
|
||||||
Intent(this, LoginActivity::class.java).apply {
|
Intent(this, LoginActivity::class.java).apply {
|
||||||
FFA.get().deleteAllData()
|
FFA.get().deleteAllData(this@MainActivity)
|
||||||
|
|
||||||
flags =
|
flags =
|
||||||
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||||
|
@ -299,8 +301,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
EventBus.get().collect { message ->
|
EventBus.get().collect { message ->
|
||||||
when (message) {
|
when (message) {
|
||||||
is Event.LogOut -> {
|
is Event.LogOut -> {
|
||||||
FFA.get().deleteAllData()
|
FFA.get().deleteAllData(this@MainActivity)
|
||||||
|
|
||||||
startActivity(Intent(this@MainActivity, LoginActivity::class.java).apply {
|
startActivity(Intent(this@MainActivity, LoginActivity::class.java).apply {
|
||||||
flags = Intent.FLAG_ACTIVITY_NO_HISTORY
|
flags = Intent.FLAG_ACTIVITY_NO_HISTORY
|
||||||
})
|
})
|
||||||
|
@ -494,10 +495,10 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.let { now_playing_details_repeat ->
|
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.let { now_playing_details_repeat ->
|
||||||
changeRepeatMode(Cache.get(this@MainActivity, "repeat")?.readLine()?.toInt() ?: 0)
|
changeRepeatMode(FFACache.get(this@MainActivity, "repeat")?.readLine()?.toInt() ?: 0)
|
||||||
|
|
||||||
now_playing_details_repeat.setOnClickListener {
|
now_playing_details_repeat.setOnClickListener {
|
||||||
val current = Cache.get(this@MainActivity, "repeat")?.readLine()?.toInt() ?: 0
|
val current = FFACache.get(this@MainActivity, "repeat")?.readLine()?.toInt() ?: 0
|
||||||
|
|
||||||
changeRepeatMode((current + 1) % 3)
|
changeRepeatMode((current + 1) % 3)
|
||||||
}
|
}
|
||||||
|
@ -577,7 +578,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
when (index) {
|
when (index) {
|
||||||
// From no repeat to repeat all
|
// From no repeat to repeat all
|
||||||
0 -> {
|
0 -> {
|
||||||
Cache.set(this@MainActivity, "repeat", "0".toByteArray())
|
FFACache.set(this@MainActivity, "repeat", "0".toByteArray())
|
||||||
|
|
||||||
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.setImageResource(R.drawable.repeat)
|
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.setImageResource(R.drawable.repeat)
|
||||||
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.setColorFilter(
|
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.setColorFilter(
|
||||||
|
@ -593,7 +594,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
// From repeat all to repeat one
|
// From repeat all to repeat one
|
||||||
1 -> {
|
1 -> {
|
||||||
Cache.set(this@MainActivity, "repeat", "1".toByteArray())
|
FFACache.set(this@MainActivity, "repeat", "1".toByteArray())
|
||||||
|
|
||||||
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.setImageResource(R.drawable.repeat)
|
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.setImageResource(R.drawable.repeat)
|
||||||
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.setColorFilter(
|
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.setColorFilter(
|
||||||
|
@ -609,7 +610,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
// From repeat one to no repeat
|
// From repeat one to no repeat
|
||||||
2 -> {
|
2 -> {
|
||||||
Cache.set(this@MainActivity, "repeat", "2".toByteArray())
|
FFACache.set(this@MainActivity, "repeat", "2".toByteArray())
|
||||||
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.setImageResource(R.drawable.repeat_one)
|
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.setImageResource(R.drawable.repeat_one)
|
||||||
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.setColorFilter(
|
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.setColorFilter(
|
||||||
ContextCompat.getColor(
|
ContextCompat.getColor(
|
||||||
|
@ -631,7 +632,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
try {
|
try {
|
||||||
Fuel
|
Fuel
|
||||||
.post(mustNormalizeUrl("/api/v1/history/listenings/"))
|
.post(mustNormalizeUrl("/api/v1/history/listenings/"))
|
||||||
.authorize(this@MainActivity)
|
.authorize(this@MainActivity, oAuth)
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.body(Gson().toJson(mapOf("track" to track.id)))
|
.body(Gson().toJson(mapOf("track" to track.id)))
|
||||||
.awaitStringResponse()
|
.awaitStringResponse()
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
package audio.funkwhale.ffa.activities
|
package audio.funkwhale.ffa.activities
|
||||||
|
|
||||||
import android.content.ClipData
|
import android.content.*
|
||||||
import android.content.ClipboardManager
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
@ -17,9 +13,9 @@ import androidx.preference.SeekBarPreference
|
||||||
import audio.funkwhale.ffa.FFA
|
import audio.funkwhale.ffa.FFA
|
||||||
import audio.funkwhale.ffa.R
|
import audio.funkwhale.ffa.R
|
||||||
import audio.funkwhale.ffa.databinding.ActivitySettingsBinding
|
import audio.funkwhale.ffa.databinding.ActivitySettingsBinding
|
||||||
import audio.funkwhale.ffa.utils.Cache
|
|
||||||
import audio.funkwhale.ffa.utils.Command
|
import audio.funkwhale.ffa.utils.Command
|
||||||
import audio.funkwhale.ffa.utils.CommandBus
|
import audio.funkwhale.ffa.utils.CommandBus
|
||||||
|
import audio.funkwhale.ffa.utils.FFACache
|
||||||
|
|
||||||
class SettingsActivity : AppCompatActivity() {
|
class SettingsActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
@ -67,7 +63,7 @@ class SettingsFragment :
|
||||||
"crash" -> {
|
"crash" -> {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
(activity.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager)?.also { clip ->
|
(activity.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager)?.also { clip ->
|
||||||
Cache.get(activity, "crashdump")?.readLines()?.joinToString("\n").also {
|
FFACache.get(activity, "crashdump")?.readLines()?.joinToString("\n").also {
|
||||||
clip.setPrimaryClip(ClipData.newPlainText("Funkwhale logs", it))
|
clip.setPrimaryClip(ClipData.newPlainText("Funkwhale logs", it))
|
||||||
|
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
|
@ -87,9 +83,7 @@ class SettingsFragment :
|
||||||
.setMessage(context.getString(R.string.logout_content))
|
.setMessage(context.getString(R.string.logout_content))
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
CommandBus.send(Command.ClearQueue)
|
CommandBus.send(Command.ClearQueue)
|
||||||
|
FFA.get().deleteAllData(context)
|
||||||
FFA.get().deleteAllData()
|
|
||||||
|
|
||||||
activity?.setResult(MainActivity.ResultCode.LOGOUT.code)
|
activity?.setResult(MainActivity.ResultCode.LOGOUT.code)
|
||||||
activity?.finish()
|
activity?.finish()
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,30 +5,28 @@ import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import audio.funkwhale.ffa.FFA
|
import audio.funkwhale.ffa.FFA
|
||||||
import audio.funkwhale.ffa.utils.AppContext
|
import audio.funkwhale.ffa.utils.*
|
||||||
import audio.funkwhale.ffa.utils.OAuth
|
import org.koin.java.KoinJavaComponent.inject
|
||||||
import audio.funkwhale.ffa.utils.OAuthFactory
|
|
||||||
import audio.funkwhale.ffa.utils.Settings
|
|
||||||
|
|
||||||
class SplashActivity : AppCompatActivity() {
|
class SplashActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private lateinit var oAuth: OAuth
|
private val oAuth: OAuth by inject(OAuth::class.java)
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
oAuth = OAuthFactory.instance()
|
|
||||||
|
|
||||||
getSharedPreferences(AppContext.PREFS_CREDENTIALS, Context.MODE_PRIVATE)
|
getSharedPreferences(AppContext.PREFS_CREDENTIALS, Context.MODE_PRIVATE)
|
||||||
.apply {
|
.apply {
|
||||||
when (oAuth.isAuthorized(this@SplashActivity) || Settings.isAnonymous()) {
|
when (oAuth.isAuthorized(this@SplashActivity) || Settings.isAnonymous()) {
|
||||||
true -> Intent(this@SplashActivity, MainActivity::class.java).apply {
|
true -> Intent(this@SplashActivity, MainActivity::class.java)
|
||||||
|
.apply {
|
||||||
flags = Intent.FLAG_ACTIVITY_NO_ANIMATION
|
flags = Intent.FLAG_ACTIVITY_NO_ANIMATION
|
||||||
startActivity(this)
|
startActivity(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
false -> Intent(this@SplashActivity, LoginActivity::class.java).apply {
|
false -> Intent(this@SplashActivity, LoginActivity::class.java)
|
||||||
FFA.get().deleteAllData()
|
.apply {
|
||||||
|
FFA.get().deleteAllData(this@SplashActivity)
|
||||||
flags = Intent.FLAG_ACTIVITY_NO_ANIMATION
|
flags = Intent.FLAG_ACTIVITY_NO_ANIMATION
|
||||||
startActivity(this)
|
startActivity(this)
|
||||||
}
|
}
|
||||||
|
|
|
@ -115,7 +115,7 @@ object AddToPlaylistDialog {
|
||||||
|
|
||||||
lifecycleScope.launch(IO) {
|
lifecycleScope.launch(IO) {
|
||||||
try {
|
try {
|
||||||
Cache.set(
|
FFACache.set(
|
||||||
context,
|
context,
|
||||||
cacheId,
|
cacheId,
|
||||||
Gson().toJson(cache(adapter.data)).toByteArray()
|
Gson().toJson(cache(adapter.data)).toByteArray()
|
||||||
|
|
|
@ -142,7 +142,7 @@ abstract class FFAFragment<D : Any, A : FFAAdapter<D, *>>() : Fragment() {
|
||||||
withContext(IO) {
|
withContext(IO) {
|
||||||
try {
|
try {
|
||||||
repository.cacheId?.let { cacheId ->
|
repository.cacheId?.let { cacheId ->
|
||||||
Cache.set(
|
FFACache.set(
|
||||||
context,
|
context,
|
||||||
cacheId,
|
cacheId,
|
||||||
Gson().toJson(repository.cache(adapter.data)).toByteArray()
|
Gson().toJson(repository.cache(adapter.data)).toByteArray()
|
||||||
|
@ -168,7 +168,7 @@ abstract class FFAFragment<D : Any, A : FFAAdapter<D, *>>() : Fragment() {
|
||||||
|
|
||||||
(repository.upstream as? HttpUpstream<*, *>)?.let { upstream ->
|
(repository.upstream as? HttpUpstream<*, *>)?.let { upstream ->
|
||||||
when (upstream.behavior) {
|
when (upstream.behavior) {
|
||||||
HttpUpstream.Behavior.Progressive -> if (!hasMore || !moreLoading) swiper?.isRefreshing =
|
HttpUpstream.Behavior.Progressive -> if (!hasMore || !moreLoading) swiper.isRefreshing =
|
||||||
false
|
false
|
||||||
HttpUpstream.Behavior.AtOnce -> if (!hasMore) swiper.isRefreshing = false
|
HttpUpstream.Behavior.AtOnce -> if (!hasMore) swiper.isRefreshing = false
|
||||||
HttpUpstream.Behavior.Single -> if (!hasMore) swiper.isRefreshing = false
|
HttpUpstream.Behavior.Single -> if (!hasMore) swiper.isRefreshing = false
|
||||||
|
|
|
@ -10,25 +10,20 @@ import audio.funkwhale.ffa.adapters.FavoritesAdapter
|
||||||
import audio.funkwhale.ffa.databinding.FragmentFavoritesBinding
|
import audio.funkwhale.ffa.databinding.FragmentFavoritesBinding
|
||||||
import audio.funkwhale.ffa.repositories.FavoritesRepository
|
import audio.funkwhale.ffa.repositories.FavoritesRepository
|
||||||
import audio.funkwhale.ffa.repositories.TracksRepository
|
import audio.funkwhale.ffa.repositories.TracksRepository
|
||||||
import audio.funkwhale.ffa.utils.Command
|
import audio.funkwhale.ffa.utils.*
|
||||||
import audio.funkwhale.ffa.utils.CommandBus
|
|
||||||
import audio.funkwhale.ffa.utils.Event
|
|
||||||
import audio.funkwhale.ffa.utils.EventBus
|
|
||||||
import audio.funkwhale.ffa.utils.Request
|
|
||||||
import audio.funkwhale.ffa.utils.RequestBus
|
|
||||||
import audio.funkwhale.ffa.utils.Response
|
|
||||||
import audio.funkwhale.ffa.utils.Track
|
|
||||||
import audio.funkwhale.ffa.utils.getMetadata
|
|
||||||
import audio.funkwhale.ffa.utils.wait
|
|
||||||
import com.google.android.exoplayer2.offline.Download
|
import com.google.android.exoplayer2.offline.Download
|
||||||
|
import com.google.android.exoplayer2.offline.DownloadManager
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.Dispatchers.Main
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
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 org.koin.java.KoinJavaComponent.inject
|
||||||
|
|
||||||
class FavoritesFragment : FFAFragment<Track, FavoritesAdapter>() {
|
class FavoritesFragment : FFAFragment<Track, FavoritesAdapter>() {
|
||||||
|
|
||||||
|
private val exoDownloadManager: DownloadManager by inject(DownloadManager::class.java)
|
||||||
|
|
||||||
private var _binding: FragmentFavoritesBinding? = null
|
private var _binding: FragmentFavoritesBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
@ -95,7 +90,7 @@ class FavoritesFragment : FFAFragment<Track, FavoritesAdapter>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun refreshDownloadedTracks() {
|
private suspend fun refreshDownloadedTracks() {
|
||||||
val downloaded = TracksRepository.getDownloadedIds() ?: listOf()
|
val downloaded = TracksRepository.getDownloadedIds(exoDownloadManager) ?: listOf()
|
||||||
|
|
||||||
withContext(Main) {
|
withContext(Main) {
|
||||||
adapter.data = adapter.data.map {
|
adapter.data = adapter.data.map {
|
||||||
|
|
|
@ -17,21 +17,9 @@ import audio.funkwhale.ffa.databinding.FragmentTracksBinding
|
||||||
import audio.funkwhale.ffa.repositories.FavoritedRepository
|
import audio.funkwhale.ffa.repositories.FavoritedRepository
|
||||||
import audio.funkwhale.ffa.repositories.FavoritesRepository
|
import audio.funkwhale.ffa.repositories.FavoritesRepository
|
||||||
import audio.funkwhale.ffa.repositories.TracksRepository
|
import audio.funkwhale.ffa.repositories.TracksRepository
|
||||||
import audio.funkwhale.ffa.utils.Album
|
import audio.funkwhale.ffa.utils.*
|
||||||
import audio.funkwhale.ffa.utils.Command
|
|
||||||
import audio.funkwhale.ffa.utils.CommandBus
|
|
||||||
import audio.funkwhale.ffa.utils.Event
|
|
||||||
import audio.funkwhale.ffa.utils.EventBus
|
|
||||||
import audio.funkwhale.ffa.utils.Request
|
|
||||||
import audio.funkwhale.ffa.utils.RequestBus
|
|
||||||
import audio.funkwhale.ffa.utils.Response
|
|
||||||
import audio.funkwhale.ffa.utils.Track
|
|
||||||
import audio.funkwhale.ffa.utils.getMetadata
|
|
||||||
import audio.funkwhale.ffa.utils.maybeLoad
|
|
||||||
import audio.funkwhale.ffa.utils.maybeNormalizeUrl
|
|
||||||
import audio.funkwhale.ffa.utils.toast
|
|
||||||
import audio.funkwhale.ffa.utils.wait
|
|
||||||
import com.google.android.exoplayer2.offline.Download
|
import com.google.android.exoplayer2.offline.Download
|
||||||
|
import com.google.android.exoplayer2.offline.DownloadManager
|
||||||
import com.preference.PowerPreference
|
import com.preference.PowerPreference
|
||||||
import com.squareup.picasso.Picasso
|
import com.squareup.picasso.Picasso
|
||||||
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
||||||
|
@ -40,9 +28,12 @@ import kotlinx.coroutines.Dispatchers.Main
|
||||||
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 org.koin.java.KoinJavaComponent.inject
|
||||||
|
|
||||||
class TracksFragment : FFAFragment<Track, TracksAdapter>() {
|
class TracksFragment : FFAFragment<Track, TracksAdapter>() {
|
||||||
|
|
||||||
|
private val exoDownloadManager: DownloadManager by inject(DownloadManager::class.java)
|
||||||
|
|
||||||
override val recycler: RecyclerView get() = binding.tracks
|
override val recycler: RecyclerView get() = binding.tracks
|
||||||
|
|
||||||
private var _binding: FragmentTracksBinding? = null
|
private var _binding: FragmentTracksBinding? = null
|
||||||
|
@ -252,7 +243,7 @@ class TracksFragment : FFAFragment<Track, TracksAdapter>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun refreshDownloadedTracks() {
|
private suspend fun refreshDownloadedTracks() {
|
||||||
val downloaded = TracksRepository.getDownloadedIds() ?: listOf()
|
val downloaded = TracksRepository.getDownloadedIds(exoDownloadManager) ?: listOf()
|
||||||
|
|
||||||
withContext(Main) {
|
withContext(Main) {
|
||||||
adapter.data = adapter.data.map {
|
adapter.data = adapter.data.map {
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
package audio.funkwhale.ffa.koin
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import audio.funkwhale.ffa.playback.CacheDataSourceFactoryProvider
|
||||||
|
import audio.funkwhale.ffa.playback.MediaSession
|
||||||
|
import audio.funkwhale.ffa.utils.AuthorizationServiceFactory
|
||||||
|
import audio.funkwhale.ffa.utils.DefaultOAuth
|
||||||
|
import audio.funkwhale.ffa.utils.OAuth
|
||||||
|
import com.google.android.exoplayer2.database.DatabaseProvider
|
||||||
|
import com.google.android.exoplayer2.database.ExoDatabaseProvider
|
||||||
|
import com.google.android.exoplayer2.offline.DefaultDownloadIndex
|
||||||
|
import com.google.android.exoplayer2.offline.DefaultDownloaderFactory
|
||||||
|
import com.google.android.exoplayer2.offline.DownloadManager
|
||||||
|
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper
|
||||||
|
import com.google.android.exoplayer2.upstream.cache.Cache
|
||||||
|
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.preference.PowerPreference
|
||||||
|
import org.koin.core.qualifier.named
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
fun ffaModule(context: Context) = module {
|
||||||
|
|
||||||
|
single<OAuth> { DefaultOAuth(get()) }
|
||||||
|
|
||||||
|
single { AuthorizationServiceFactory() }
|
||||||
|
|
||||||
|
single {
|
||||||
|
val cacheDataSourceFactoryProvider = get<CacheDataSourceFactoryProvider>()
|
||||||
|
DownloaderConstructorHelper(
|
||||||
|
get(named("exoDownloadCache")), cacheDataSourceFactoryProvider.create(context)
|
||||||
|
).run {
|
||||||
|
DownloadManager(
|
||||||
|
context,
|
||||||
|
DefaultDownloadIndex(get(named("exoDatabase"))),
|
||||||
|
DefaultDownloaderFactory(this)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
single {
|
||||||
|
CacheDataSourceFactoryProvider(
|
||||||
|
get(),
|
||||||
|
get(named("exoCache")),
|
||||||
|
get(named("exoDownloadCache"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
single<DatabaseProvider>(named("exoDatabase")) { ExoDatabaseProvider(context) }
|
||||||
|
|
||||||
|
single<Cache>(named("exoDownloadCache")) {
|
||||||
|
SimpleCache(
|
||||||
|
context.cacheDir.resolve("downloads"),
|
||||||
|
NoOpCacheEvictor(),
|
||||||
|
get<DatabaseProvider>(named("exoDatabase"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
single<Cache>(named("exoCache")) {
|
||||||
|
val cacheSize = PowerPreference.getDefaultFile().getInt("media_cache_size", 1).toLong()
|
||||||
|
.let { if (it == 0L) 0 else it * 1024 * 1024 * 1024 }
|
||||||
|
SimpleCache(
|
||||||
|
context.cacheDir.resolve("media"),
|
||||||
|
LeastRecentlyUsedCacheEvictor(cacheSize),
|
||||||
|
get<DatabaseProvider>(named("exoDatabase"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
single { MediaSession(context) }
|
||||||
|
}
|
|
@ -10,7 +10,6 @@ import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.media.app.NotificationCompat.MediaStyle
|
import androidx.media.app.NotificationCompat.MediaStyle
|
||||||
import androidx.media.session.MediaButtonReceiver
|
import androidx.media.session.MediaButtonReceiver
|
||||||
import audio.funkwhale.ffa.FFA
|
|
||||||
import audio.funkwhale.ffa.R
|
import audio.funkwhale.ffa.R
|
||||||
import audio.funkwhale.ffa.activities.MainActivity
|
import audio.funkwhale.ffa.activities.MainActivity
|
||||||
import audio.funkwhale.ffa.utils.AppContext
|
import audio.funkwhale.ffa.utils.AppContext
|
||||||
|
@ -20,12 +19,16 @@ import com.squareup.picasso.Picasso
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers.Default
|
import kotlinx.coroutines.Dispatchers.Default
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.java.KoinJavaComponent.inject
|
||||||
|
|
||||||
class MediaControlsManager(val context: Service, private val scope: CoroutineScope, private val mediaSession: MediaSessionCompat) {
|
class MediaControlsManager(val context: Service, private val scope: CoroutineScope, private val mediaSession: MediaSessionCompat) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val NOTIFICATION_ACTION_OPEN_QUEUE = 0
|
const val NOTIFICATION_ACTION_OPEN_QUEUE = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val ffaMediaSession: MediaSession by inject(MediaSession::class.java)
|
||||||
|
|
||||||
private var notification: Notification? = null
|
private var notification: Notification? = null
|
||||||
|
|
||||||
fun updateNotification(track: Track?, playing: Boolean) {
|
fun updateNotification(track: Track?, playing: Boolean) {
|
||||||
|
@ -99,7 +102,7 @@ class MediaControlsManager(val context: Service, private val scope: CoroutineSco
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FFA.get().mediaSession.connector.invalidateMediaSessionMetadata()
|
ffaMediaSession.connector.invalidateMediaSessionMetadata()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,17 +4,8 @@ import android.app.Notification
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import audio.funkwhale.ffa.FFA
|
|
||||||
import audio.funkwhale.ffa.R
|
import audio.funkwhale.ffa.R
|
||||||
import audio.funkwhale.ffa.utils.AppContext
|
import audio.funkwhale.ffa.utils.*
|
||||||
import audio.funkwhale.ffa.utils.DownloadInfo
|
|
||||||
import audio.funkwhale.ffa.utils.Event
|
|
||||||
import audio.funkwhale.ffa.utils.EventBus
|
|
||||||
import audio.funkwhale.ffa.utils.Request
|
|
||||||
import audio.funkwhale.ffa.utils.RequestBus
|
|
||||||
import audio.funkwhale.ffa.utils.Response
|
|
||||||
import audio.funkwhale.ffa.utils.Track
|
|
||||||
import audio.funkwhale.ffa.utils.mustNormalizeUrl
|
|
||||||
import com.google.android.exoplayer2.offline.Download
|
import com.google.android.exoplayer2.offline.Download
|
||||||
import com.google.android.exoplayer2.offline.DownloadManager
|
import com.google.android.exoplayer2.offline.DownloadManager
|
||||||
import com.google.android.exoplayer2.offline.DownloadRequest
|
import com.google.android.exoplayer2.offline.DownloadRequest
|
||||||
|
@ -27,10 +18,13 @@ import kotlinx.coroutines.Dispatchers.Main
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.Collections
|
import org.koin.java.KoinJavaComponent
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
class PinService : DownloadService(AppContext.NOTIFICATION_DOWNLOADS) {
|
class PinService : DownloadService(AppContext.NOTIFICATION_DOWNLOADS) {
|
||||||
|
|
||||||
private val scope: CoroutineScope = CoroutineScope(Job() + Main)
|
private val scope: CoroutineScope = CoroutineScope(Job() + Main)
|
||||||
|
private val exoDownloadManager: DownloadManager by KoinJavaComponent.inject(DownloadManager::class.java)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun download(context: Context, track: Track) {
|
fun download(context: Context, track: Track) {
|
||||||
|
@ -74,7 +68,7 @@ class PinService : DownloadService(AppContext.NOTIFICATION_DOWNLOADS) {
|
||||||
return super.onStartCommand(intent, flags, startId)
|
return super.onStartCommand(intent, flags, startId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getDownloadManager() = FFA.get().exoDownloadManager.apply {
|
override fun getDownloadManager() = exoDownloadManager.apply {
|
||||||
addListener(DownloadListener())
|
addListener(DownloadListener())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,6 @@ import android.support.v4.media.MediaMetadataCompat
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.media.session.MediaButtonReceiver
|
import androidx.media.session.MediaButtonReceiver
|
||||||
import audio.funkwhale.ffa.FFA
|
|
||||||
import audio.funkwhale.ffa.R
|
import audio.funkwhale.ffa.R
|
||||||
import audio.funkwhale.ffa.utils.*
|
import audio.funkwhale.ffa.utils.*
|
||||||
import com.google.android.exoplayer2.C
|
import com.google.android.exoplayer2.C
|
||||||
|
@ -29,12 +28,15 @@ import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.Dispatchers.Main
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import org.koin.java.KoinJavaComponent.inject
|
||||||
|
|
||||||
class PlayerService : Service() {
|
class PlayerService : Service() {
|
||||||
companion object {
|
companion object {
|
||||||
const val INITIAL_COMMAND_KEY = "start_command"
|
const val INITIAL_COMMAND_KEY = "start_command"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val mediaSession: MediaSession by inject(MediaSession::class.java)
|
||||||
|
|
||||||
private var started = false
|
private var started = false
|
||||||
private val scope: CoroutineScope = CoroutineScope(Job() + Main)
|
private val scope: CoroutineScope = CoroutineScope(Job() + Main)
|
||||||
|
|
||||||
|
@ -63,12 +65,12 @@ class PlayerService : Service() {
|
||||||
when (key.keyCode) {
|
when (key.keyCode) {
|
||||||
KeyEvent.KEYCODE_MEDIA_PLAY, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
|
KeyEvent.KEYCODE_MEDIA_PLAY, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
|
||||||
if (hasAudioFocus(true)) MediaButtonReceiver.handleIntent(
|
if (hasAudioFocus(true)) MediaButtonReceiver.handleIntent(
|
||||||
FFA.get().mediaSession.session,
|
mediaSession.session,
|
||||||
intent
|
intent
|
||||||
)
|
)
|
||||||
Unit
|
Unit
|
||||||
}
|
}
|
||||||
else -> MediaButtonReceiver.handleIntent(FFA.get().mediaSession.session, intent)
|
else -> MediaButtonReceiver.handleIntent(mediaSession.session, intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -108,7 +110,7 @@ class PlayerService : Service() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaControlsManager = MediaControlsManager(this, scope, FFA.get().mediaSession.session)
|
mediaControlsManager = MediaControlsManager(this, scope, mediaSession.session)
|
||||||
|
|
||||||
player = SimpleExoPlayer.Builder(this).build().apply {
|
player = SimpleExoPlayer.Builder(this).build().apply {
|
||||||
playWhenReady = false
|
playWhenReady = false
|
||||||
|
@ -118,9 +120,9 @@ class PlayerService : Service() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FFA.get().mediaSession.active = true
|
mediaSession.active = true
|
||||||
|
|
||||||
FFA.get().mediaSession.connector.apply {
|
mediaSession.connector.apply {
|
||||||
setPlayer(player)
|
setPlayer(player)
|
||||||
|
|
||||||
setMediaMetadataProvider {
|
setMediaMetadataProvider {
|
||||||
|
@ -129,9 +131,9 @@ class PlayerService : Service() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (queue.current > -1) {
|
if (queue.current > -1) {
|
||||||
player.prepare(queue.datasources)
|
player.prepare(queue.dataSources)
|
||||||
|
|
||||||
Cache.get(this, "progress")?.let { progress ->
|
FFACache.get(this, "progress")?.let { progress ->
|
||||||
player.seekTo(queue.current, progress.readLine().toLong())
|
player.seekTo(queue.current, progress.readLine().toLong())
|
||||||
|
|
||||||
val (current, duration, percent) = getProgress(true)
|
val (current, duration, percent) = getProgress(true)
|
||||||
|
@ -161,7 +163,7 @@ class PlayerService : Service() {
|
||||||
if (!command.fromRadio) radioPlayer.stop()
|
if (!command.fromRadio) radioPlayer.stop()
|
||||||
|
|
||||||
queue.replace(command.queue)
|
queue.replace(command.queue)
|
||||||
player.prepare(queue.datasources, true, true)
|
player.prepare(queue.dataSources, true, true)
|
||||||
|
|
||||||
setPlaybackState(true)
|
setPlaybackState(true)
|
||||||
|
|
||||||
|
@ -271,7 +273,7 @@ class PlayerService : Service() {
|
||||||
setPlaybackState(false)
|
setPlaybackState(false)
|
||||||
player.release()
|
player.release()
|
||||||
|
|
||||||
FFA.get().mediaSession.active = false
|
mediaSession.active = false
|
||||||
|
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
@ -280,11 +282,11 @@ class PlayerService : Service() {
|
||||||
if (!state) {
|
if (!state) {
|
||||||
val (progress, _, _) = getProgress()
|
val (progress, _, _) = getProgress()
|
||||||
|
|
||||||
Cache.set(this@PlayerService, "progress", progress.toString().toByteArray())
|
FFACache.set(this@PlayerService, "progress", progress.toString().toByteArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state && player.playbackState == Player.STATE_IDLE) {
|
if (state && player.playbackState == Player.STATE_IDLE) {
|
||||||
player.prepare(queue.datasources)
|
player.prepare(queue.dataSources)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasAudioFocus(state)) {
|
if (hasAudioFocus(state)) {
|
||||||
|
@ -309,7 +311,7 @@ class PlayerService : Service() {
|
||||||
private fun skipToNextTrack() {
|
private fun skipToNextTrack() {
|
||||||
player.next()
|
player.next()
|
||||||
|
|
||||||
Cache.set(this@PlayerService, "progress", "0".toByteArray())
|
FFACache.set(this@PlayerService, "progress", "0".toByteArray())
|
||||||
ProgressBus.send(0, 0, 0)
|
ProgressBus.send(0, 0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -468,7 +470,7 @@ class PlayerService : Service() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Cache.set(this@PlayerService, "current", queue.current.toString().toByteArray())
|
FFACache.set(this@PlayerService, "current", queue.current.toString().toByteArray())
|
||||||
|
|
||||||
CommandBus.send(Command.RefreshTrack(queue.current()))
|
CommandBus.send(Command.RefreshTrack(queue.current()))
|
||||||
}
|
}
|
||||||
|
@ -486,7 +488,7 @@ class PlayerService : Service() {
|
||||||
|
|
||||||
if (player.playWhenReady) {
|
if (player.playWhenReady) {
|
||||||
queue.current++
|
queue.current++
|
||||||
player.prepare(queue.datasources, true, true)
|
player.prepare(queue.dataSources, true, true)
|
||||||
player.seekTo(queue.current, 0)
|
player.seekTo(queue.current, 0)
|
||||||
|
|
||||||
CommandBus.send(Command.RefreshTrack(queue.current()))
|
CommandBus.send(Command.RefreshTrack(queue.current()))
|
||||||
|
|
|
@ -2,18 +2,8 @@ package audio.funkwhale.ffa.playback
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import audio.funkwhale.ffa.FFA
|
|
||||||
import audio.funkwhale.ffa.R
|
import audio.funkwhale.ffa.R
|
||||||
import audio.funkwhale.ffa.utils.Cache
|
import audio.funkwhale.ffa.utils.*
|
||||||
import audio.funkwhale.ffa.utils.Command
|
|
||||||
import audio.funkwhale.ffa.utils.CommandBus
|
|
||||||
import audio.funkwhale.ffa.utils.Event
|
|
||||||
import audio.funkwhale.ffa.utils.EventBus
|
|
||||||
import audio.funkwhale.ffa.utils.OAuthFactory
|
|
||||||
import audio.funkwhale.ffa.utils.QueueCache
|
|
||||||
import audio.funkwhale.ffa.utils.Settings
|
|
||||||
import audio.funkwhale.ffa.utils.Track
|
|
||||||
import audio.funkwhale.ffa.utils.mustNormalizeUrl
|
|
||||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
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
|
||||||
|
@ -22,23 +12,24 @@ import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory
|
||||||
import com.google.android.exoplayer2.upstream.FileDataSource
|
import com.google.android.exoplayer2.upstream.FileDataSource
|
||||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSource
|
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.upstream.cache.Cache
|
||||||
import com.google.android.exoplayer2.util.Util
|
import com.google.android.exoplayer2.util.Util
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
|
import org.koin.java.KoinJavaComponent.inject
|
||||||
|
|
||||||
class QueueManager(val context: Context) {
|
class CacheDataSourceFactoryProvider(
|
||||||
var metadata: MutableList<Track> = mutableListOf()
|
private val oAuth: OAuth,
|
||||||
val datasources = ConcatenatingMediaSource()
|
private val exoCache: Cache,
|
||||||
var current = -1
|
private val exoDownloadCache: Cache
|
||||||
|
) {
|
||||||
|
|
||||||
companion object {
|
fun create(context: Context): CacheDataSourceFactory {
|
||||||
|
|
||||||
fun factory(context: Context): CacheDataSourceFactory {
|
|
||||||
|
|
||||||
val playbackCache =
|
val playbackCache =
|
||||||
CacheDataSourceFactory(FFA.get().exoCache, createDatasourceFactory(context))
|
CacheDataSourceFactory(exoCache, createDatasourceFactory(context, oAuth))
|
||||||
|
|
||||||
return CacheDataSourceFactory(
|
return CacheDataSourceFactory(
|
||||||
FFA.get().exoDownloadCache,
|
exoDownloadCache,
|
||||||
playbackCache,
|
playbackCache,
|
||||||
FileDataSource.Factory(),
|
FileDataSource.Factory(),
|
||||||
null,
|
null,
|
||||||
|
@ -47,26 +38,36 @@ class QueueManager(val context: Context) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createDatasourceFactory(context: Context): DataSource.Factory {
|
private fun createDatasourceFactory(context: Context, oAuth: OAuth): DataSource.Factory {
|
||||||
val http = DefaultHttpDataSourceFactory(
|
val http = DefaultHttpDataSourceFactory(
|
||||||
Util.getUserAgent(context, context.getString(R.string.app_name))
|
Util.getUserAgent(context, context.getString(R.string.app_name))
|
||||||
)
|
)
|
||||||
return if (!Settings.isAnonymous()) {
|
return if (!Settings.isAnonymous()) {
|
||||||
OAuth2DatasourceFactory(context, http, OAuthFactory.instance())
|
OAuth2DatasourceFactory(context, http, oAuth)
|
||||||
} else {
|
} else {
|
||||||
http
|
http
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class QueueManager(val context: Context) {
|
||||||
|
|
||||||
|
private val cacheDataSourceFactoryProvider: CacheDataSourceFactoryProvider by inject(
|
||||||
|
CacheDataSourceFactoryProvider::class.java
|
||||||
|
)
|
||||||
|
|
||||||
|
var metadata: MutableList<Track> = mutableListOf()
|
||||||
|
val dataSources = ConcatenatingMediaSource()
|
||||||
|
var current = -1
|
||||||
|
|
||||||
init {
|
init {
|
||||||
Cache.get(context, "queue")?.let { json ->
|
FFACache.get(context, "queue")?.let { json ->
|
||||||
gsonDeserializerOf(QueueCache::class.java).deserialize(json)?.let { cache ->
|
gsonDeserializerOf(QueueCache::class.java).deserialize(json)?.let { cache ->
|
||||||
metadata = cache.data.toMutableList()
|
metadata = cache.data.toMutableList()
|
||||||
|
|
||||||
val factory = factory(context)
|
val factory = cacheDataSourceFactoryProvider.create(context)
|
||||||
|
|
||||||
datasources.addMediaSources(metadata.map { track ->
|
dataSources.addMediaSources(metadata.map { track ->
|
||||||
val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "")
|
val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "")
|
||||||
|
|
||||||
ProgressiveMediaSource.Factory(factory).setTag(track.title)
|
ProgressiveMediaSource.Factory(factory).setTag(track.title)
|
||||||
|
@ -75,13 +76,13 @@ class QueueManager(val context: Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Cache.get(context, "current")?.let { string ->
|
FFACache.get(context, "current")?.let { string ->
|
||||||
current = string.readLine().toInt()
|
current = string.readLine().toInt()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun persist() {
|
private fun persist() {
|
||||||
Cache.set(
|
FFACache.set(
|
||||||
context,
|
context,
|
||||||
"queue",
|
"queue",
|
||||||
Gson().toJson(QueueCache(metadata)).toByteArray()
|
Gson().toJson(QueueCache(metadata)).toByteArray()
|
||||||
|
@ -89,8 +90,7 @@ class QueueManager(val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun replace(tracks: List<Track>) {
|
fun replace(tracks: List<Track>) {
|
||||||
val factory = factory(context)
|
val factory = cacheDataSourceFactoryProvider.create(context)
|
||||||
|
|
||||||
val sources = tracks.map { track ->
|
val sources = tracks.map { track ->
|
||||||
val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "")
|
val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "")
|
||||||
|
|
||||||
|
@ -98,8 +98,8 @@ class QueueManager(val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
metadata = tracks.toMutableList()
|
metadata = tracks.toMutableList()
|
||||||
datasources.clear()
|
dataSources.clear()
|
||||||
datasources.addMediaSources(sources)
|
dataSources.addMediaSources(sources)
|
||||||
|
|
||||||
persist()
|
persist()
|
||||||
|
|
||||||
|
@ -107,7 +107,7 @@ class QueueManager(val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun append(tracks: List<Track>) {
|
fun append(tracks: List<Track>) {
|
||||||
val factory = factory(context)
|
val factory = cacheDataSourceFactoryProvider.create(context)
|
||||||
val missingTracks = tracks.filter { metadata.indexOf(it) == -1 }
|
val missingTracks = tracks.filter { metadata.indexOf(it) == -1 }
|
||||||
|
|
||||||
val sources = missingTracks.map { track ->
|
val sources = missingTracks.map { track ->
|
||||||
|
@ -117,7 +117,7 @@ class QueueManager(val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
metadata.addAll(tracks)
|
metadata.addAll(tracks)
|
||||||
datasources.addMediaSources(sources)
|
dataSources.addMediaSources(sources)
|
||||||
|
|
||||||
persist()
|
persist()
|
||||||
|
|
||||||
|
@ -125,12 +125,12 @@ class QueueManager(val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun insertNext(track: Track) {
|
fun insertNext(track: Track) {
|
||||||
val factory = factory(context)
|
val factory = cacheDataSourceFactoryProvider.create(context)
|
||||||
val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "")
|
val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "")
|
||||||
|
|
||||||
if (metadata.indexOf(track) == -1) {
|
if (metadata.indexOf(track) == -1) {
|
||||||
ProgressiveMediaSource.Factory(factory).createMediaSource(Uri.parse(url)).let {
|
ProgressiveMediaSource.Factory(factory).createMediaSource(Uri.parse(url)).let {
|
||||||
datasources.addMediaSource(current + 1, it)
|
dataSources.addMediaSource(current + 1, it)
|
||||||
metadata.add(current + 1, track)
|
metadata.add(current + 1, track)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -148,7 +148,7 @@ class QueueManager(val context: Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
datasources.removeMediaSource(it)
|
dataSources.removeMediaSource(it)
|
||||||
metadata.removeAt(it)
|
metadata.removeAt(it)
|
||||||
|
|
||||||
if (it == current) {
|
if (it == current) {
|
||||||
|
@ -170,7 +170,7 @@ class QueueManager(val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun move(oldPosition: Int, newPosition: Int) {
|
fun move(oldPosition: Int, newPosition: Int) {
|
||||||
datasources.moveMediaSource(oldPosition, newPosition)
|
dataSources.moveMediaSource(oldPosition, newPosition)
|
||||||
metadata.add(newPosition, metadata.removeAt(oldPosition))
|
metadata.add(newPosition, metadata.removeAt(oldPosition))
|
||||||
|
|
||||||
persist()
|
persist()
|
||||||
|
@ -193,7 +193,7 @@ class QueueManager(val context: Context) {
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
metadata = mutableListOf()
|
metadata = mutableListOf()
|
||||||
datasources.clear()
|
dataSources.clear()
|
||||||
current = -1
|
current = -1
|
||||||
|
|
||||||
persist()
|
persist()
|
||||||
|
@ -214,7 +214,7 @@ class QueueManager(val context: Context) {
|
||||||
.shuffled()
|
.shuffled()
|
||||||
|
|
||||||
while (metadata.size > 1) {
|
while (metadata.size > 1) {
|
||||||
datasources.removeMediaSource(metadata.size - 1)
|
dataSources.removeMediaSource(metadata.size - 1)
|
||||||
metadata.removeAt(metadata.size - 1)
|
metadata.removeAt(metadata.size - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,16 +4,7 @@ import android.content.Context
|
||||||
import audio.funkwhale.ffa.R
|
import audio.funkwhale.ffa.R
|
||||||
import audio.funkwhale.ffa.repositories.FavoritedRepository
|
import audio.funkwhale.ffa.repositories.FavoritedRepository
|
||||||
import audio.funkwhale.ffa.repositories.Repository
|
import audio.funkwhale.ffa.repositories.Repository
|
||||||
import audio.funkwhale.ffa.utils.Cache
|
import audio.funkwhale.ffa.utils.*
|
||||||
import audio.funkwhale.ffa.utils.Command
|
|
||||||
import audio.funkwhale.ffa.utils.CommandBus
|
|
||||||
import audio.funkwhale.ffa.utils.Event
|
|
||||||
import audio.funkwhale.ffa.utils.EventBus
|
|
||||||
import audio.funkwhale.ffa.utils.Radio
|
|
||||||
import audio.funkwhale.ffa.utils.Track
|
|
||||||
import audio.funkwhale.ffa.utils.authorize
|
|
||||||
import audio.funkwhale.ffa.utils.mustNormalizeUrl
|
|
||||||
import audio.funkwhale.ffa.utils.toast
|
|
||||||
import com.github.kittinunf.fuel.Fuel
|
import com.github.kittinunf.fuel.Fuel
|
||||||
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
|
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
|
||||||
import com.github.kittinunf.fuel.coroutines.awaitObjectResult
|
import com.github.kittinunf.fuel.coroutines.awaitObjectResult
|
||||||
|
@ -27,8 +18,14 @@ import kotlinx.coroutines.flow.toList
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Semaphore
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.koin.java.KoinJavaComponent.inject
|
||||||
|
|
||||||
|
data class RadioSessionBody(
|
||||||
|
val radio_type: String,
|
||||||
|
var custom_radio: Int? = null,
|
||||||
|
var related_object_id: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
data class RadioSessionBody(val radio_type: String, var custom_radio: Int? = null, var related_object_id: String? = null)
|
|
||||||
data class RadioSession(val id: Int)
|
data class RadioSession(val id: Int)
|
||||||
data class RadioTrackBody(val session: Int)
|
data class RadioTrackBody(val session: Int)
|
||||||
data class RadioTrack(val position: Int, val track: RadioTrackID)
|
data class RadioTrack(val position: Int, val track: RadioTrackID)
|
||||||
|
@ -37,6 +34,7 @@ data class RadioTrackID(val id: Int)
|
||||||
class RadioPlayer(val context: Context, val scope: CoroutineScope) {
|
class RadioPlayer(val context: Context, val scope: CoroutineScope) {
|
||||||
val lock = Semaphore(1)
|
val lock = Semaphore(1)
|
||||||
|
|
||||||
|
private val oAuth: OAuth by inject(OAuth::class.java)
|
||||||
private var currentRadio: Radio? = null
|
private var currentRadio: Radio? = null
|
||||||
private var session: Int? = null
|
private var session: Int? = null
|
||||||
private var cookie: String? = null
|
private var cookie: String? = null
|
||||||
|
@ -44,10 +42,10 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) {
|
||||||
private val favoritedRepository = FavoritedRepository(context)
|
private val favoritedRepository = FavoritedRepository(context)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
Cache.get(context, "radio_type")?.readLine()?.let { radio_type ->
|
FFACache.get(context, "radio_type")?.readLine()?.let { radio_type ->
|
||||||
Cache.get(context, "radio_id")?.readLine()?.toInt()?.let { radio_id ->
|
FFACache.get(context, "radio_id")?.readLine()?.toInt()?.let { radio_id ->
|
||||||
Cache.get(context, "radio_session")?.readLine()?.toInt()?.let { radio_session ->
|
FFACache.get(context, "radio_session")?.readLine()?.toInt()?.let { radio_session ->
|
||||||
val cachedCookie = Cache.get(context, "radio_cookie")?.readLine()
|
val cachedCookie = FFACache.get(context, "radio_cookie")?.readLine()
|
||||||
|
|
||||||
currentRadio = Radio(radio_id, radio_type, "", "")
|
currentRadio = Radio(radio_id, radio_type, "", "")
|
||||||
session = radio_session
|
session = radio_session
|
||||||
|
@ -70,10 +68,10 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) {
|
||||||
currentRadio = null
|
currentRadio = null
|
||||||
session = null
|
session = null
|
||||||
|
|
||||||
Cache.delete(context, "radio_type")
|
FFACache.delete(context, "radio_type")
|
||||||
Cache.delete(context, "radio_id")
|
FFACache.delete(context, "radio_id")
|
||||||
Cache.delete(context, "radio_session")
|
FFACache.delete(context, "radio_session")
|
||||||
Cache.delete(context, "radio_cookie")
|
FFACache.delete(context, "radio_cookie")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isActive() = currentRadio != null && session != null
|
fun isActive() = currentRadio != null && session != null
|
||||||
|
@ -81,7 +79,8 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) {
|
||||||
private suspend fun createSession() {
|
private suspend fun createSession() {
|
||||||
currentRadio?.let { radio ->
|
currentRadio?.let { radio ->
|
||||||
try {
|
try {
|
||||||
val request = RadioSessionBody(radio.radio_type, related_object_id = radio.related_object_id).apply {
|
val request =
|
||||||
|
RadioSessionBody(radio.radio_type, related_object_id = radio.related_object_id).apply {
|
||||||
if (radio_type == "custom") {
|
if (radio_type == "custom") {
|
||||||
custom_radio = radio.id
|
custom_radio = radio.id
|
||||||
}
|
}
|
||||||
|
@ -89,7 +88,7 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) {
|
||||||
|
|
||||||
val body = Gson().toJson(request)
|
val body = Gson().toJson(request)
|
||||||
val (_, response, result) = Fuel.post(mustNormalizeUrl("/api/v1/radios/sessions/"))
|
val (_, response, result) = Fuel.post(mustNormalizeUrl("/api/v1/radios/sessions/"))
|
||||||
.authorize(context)
|
.authorize(context, oAuth)
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.body(body)
|
.body(body)
|
||||||
.awaitObjectResponseResult(gsonDeserializerOf(RadioSession::class.java))
|
.awaitObjectResponseResult(gsonDeserializerOf(RadioSession::class.java))
|
||||||
|
@ -97,10 +96,10 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) {
|
||||||
session = result.get().id
|
session = result.get().id
|
||||||
cookie = response.header("set-cookie").joinToString(";")
|
cookie = response.header("set-cookie").joinToString(";")
|
||||||
|
|
||||||
Cache.set(context, "radio_type", radio.radio_type.toByteArray())
|
FFACache.set(context, "radio_type", radio.radio_type.toByteArray())
|
||||||
Cache.set(context, "radio_id", radio.id.toString().toByteArray())
|
FFACache.set(context, "radio_id", radio.id.toString().toByteArray())
|
||||||
Cache.set(context, "radio_session", session.toString().toByteArray())
|
FFACache.set(context, "radio_session", session.toString().toByteArray())
|
||||||
Cache.set(context, "radio_cookie", cookie.toString().toByteArray())
|
FFACache.set(context, "radio_cookie", cookie.toString().toByteArray())
|
||||||
|
|
||||||
prepareNextTrack(true)
|
prepareNextTrack(true)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -116,7 +115,7 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) {
|
||||||
try {
|
try {
|
||||||
val body = Gson().toJson(RadioTrackBody(session))
|
val body = Gson().toJson(RadioTrackBody(session))
|
||||||
val result = Fuel.post(mustNormalizeUrl("/api/v1/radios/tracks/"))
|
val result = Fuel.post(mustNormalizeUrl("/api/v1/radios/tracks/"))
|
||||||
.authorize(context)
|
.authorize(context, oAuth)
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.apply {
|
.apply {
|
||||||
cookie?.let {
|
cookie?.let {
|
||||||
|
@ -127,7 +126,7 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) {
|
||||||
.awaitObjectResult(gsonDeserializerOf(RadioTrack::class.java))
|
.awaitObjectResult(gsonDeserializerOf(RadioTrack::class.java))
|
||||||
|
|
||||||
val trackResponse = Fuel.get(mustNormalizeUrl("/api/v1/tracks/${result.get().track.id}/"))
|
val trackResponse = Fuel.get(mustNormalizeUrl("/api/v1/tracks/${result.get().track.id}/"))
|
||||||
.authorize(context)
|
.authorize(context, oAuth)
|
||||||
.awaitObjectResult(gsonDeserializerOf(Track::class.java))
|
.awaitObjectResult(gsonDeserializerOf(Track::class.java))
|
||||||
|
|
||||||
val favorites = favoritedRepository.fetch(Repository.Origin.Cache.origin)
|
val favorites = favoritedRepository.fetch(Repository.Origin.Cache.origin)
|
||||||
|
|
|
@ -4,15 +4,16 @@ import android.content.Context
|
||||||
import audio.funkwhale.ffa.utils.Album
|
import audio.funkwhale.ffa.utils.Album
|
||||||
import audio.funkwhale.ffa.utils.AlbumsCache
|
import audio.funkwhale.ffa.utils.AlbumsCache
|
||||||
import audio.funkwhale.ffa.utils.AlbumsResponse
|
import audio.funkwhale.ffa.utils.AlbumsResponse
|
||||||
import audio.funkwhale.ffa.utils.OAuthFactory
|
import audio.funkwhale.ffa.utils.OAuth
|
||||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
|
import org.koin.java.KoinJavaComponent.inject
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
|
|
||||||
class AlbumsRepository(override val context: Context?, artistId: Int? = null) :
|
class AlbumsRepository(override val context: Context?, artistId: Int? = null) :
|
||||||
Repository<Album, AlbumsCache>() {
|
Repository<Album, AlbumsCache>() {
|
||||||
|
|
||||||
private val oAuth = OAuthFactory.instance()
|
private val oAuth: OAuth by inject(OAuth::class.java)
|
||||||
|
|
||||||
override val cacheId: String by lazy {
|
override val cacheId: String by lazy {
|
||||||
if (artistId == null) "albums"
|
if (artistId == null) "albums"
|
||||||
|
|
|
@ -1,19 +1,16 @@
|
||||||
package audio.funkwhale.ffa.repositories
|
package audio.funkwhale.ffa.repositories
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import audio.funkwhale.ffa.utils.OAuthFactory
|
import audio.funkwhale.ffa.utils.*
|
||||||
import audio.funkwhale.ffa.utils.OtterResponse
|
|
||||||
import audio.funkwhale.ffa.utils.Track
|
|
||||||
import audio.funkwhale.ffa.utils.TracksCache
|
|
||||||
import audio.funkwhale.ffa.utils.TracksResponse
|
|
||||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
|
import org.koin.java.KoinJavaComponent.inject
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
|
|
||||||
class ArtistTracksRepository(override val context: Context?, private val artistId: Int) :
|
class ArtistTracksRepository(override val context: Context?, private val artistId: Int) :
|
||||||
Repository<Track, TracksCache>() {
|
Repository<Track, TracksCache>() {
|
||||||
|
|
||||||
private val oAuth = OAuthFactory.instance()
|
private val oAuth: OAuth by inject(OAuth::class.java)
|
||||||
|
|
||||||
override val cacheId = "tracks-artist-$artistId"
|
override val cacheId = "tracks-artist-$artistId"
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,15 @@
|
||||||
package audio.funkwhale.ffa.repositories
|
package audio.funkwhale.ffa.repositories
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import audio.funkwhale.ffa.utils.Artist
|
import audio.funkwhale.ffa.utils.*
|
||||||
import audio.funkwhale.ffa.utils.ArtistsCache
|
|
||||||
import audio.funkwhale.ffa.utils.ArtistsResponse
|
|
||||||
import audio.funkwhale.ffa.utils.OAuthFactory
|
|
||||||
import audio.funkwhale.ffa.utils.OtterResponse
|
|
||||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
|
import org.koin.java.KoinJavaComponent.inject
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
|
|
||||||
class ArtistsRepository(override val context: Context?) : Repository<Artist, ArtistsCache>() {
|
class ArtistsRepository(override val context: Context?) : Repository<Artist, ArtistsCache>() {
|
||||||
|
|
||||||
private val oAuth = OAuthFactory.instance()
|
private val oAuth: OAuth by inject(OAuth::class.java)
|
||||||
|
|
||||||
override val cacheId = "artists"
|
override val cacheId = "artists"
|
||||||
|
|
||||||
|
|
|
@ -1,34 +1,28 @@
|
||||||
package audio.funkwhale.ffa.repositories
|
package audio.funkwhale.ffa.repositories
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import audio.funkwhale.ffa.FFA
|
import audio.funkwhale.ffa.utils.*
|
||||||
import audio.funkwhale.ffa.utils.Cache
|
|
||||||
import audio.funkwhale.ffa.utils.FavoritedCache
|
|
||||||
import audio.funkwhale.ffa.utils.FavoritedResponse
|
|
||||||
import audio.funkwhale.ffa.utils.OAuthFactory
|
|
||||||
import audio.funkwhale.ffa.utils.OtterResponse
|
|
||||||
import audio.funkwhale.ffa.utils.Settings
|
|
||||||
import audio.funkwhale.ffa.utils.Track
|
|
||||||
import audio.funkwhale.ffa.utils.TracksCache
|
|
||||||
import audio.funkwhale.ffa.utils.TracksResponse
|
|
||||||
import audio.funkwhale.ffa.utils.authorize
|
|
||||||
import audio.funkwhale.ffa.utils.maybeNormalizeUrl
|
|
||||||
import audio.funkwhale.ffa.utils.mustNormalizeUrl
|
|
||||||
import audio.funkwhale.ffa.utils.untilNetwork
|
|
||||||
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
|
||||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||||
|
import com.google.android.exoplayer2.offline.DownloadManager
|
||||||
|
import com.google.android.exoplayer2.upstream.cache.Cache
|
||||||
|
import com.google.android.exoplayer2.upstream.cache.SimpleCache
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.koin.core.qualifier.named
|
||||||
|
import org.koin.java.KoinJavaComponent.inject
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
|
|
||||||
class FavoritesRepository(override val context: Context?) : Repository<Track, TracksCache>() {
|
class FavoritesRepository(override val context: Context?) : Repository<Track, TracksCache>() {
|
||||||
|
|
||||||
private var oAuth = OAuthFactory.instance()
|
private val exoDownloadManager: DownloadManager by inject(DownloadManager::class.java)
|
||||||
|
private val exoCache: Cache by inject(Cache::class.java, named("exoCache"))
|
||||||
|
private val oAuth: OAuth by inject(OAuth::class.java)
|
||||||
|
|
||||||
override val cacheId = "favorites.v2"
|
override val cacheId = "favorites.v2"
|
||||||
|
|
||||||
|
@ -47,7 +41,7 @@ class FavoritesRepository(override val context: Context?) : Repository<Track, Tr
|
||||||
private val favoritedRepository = FavoritedRepository(context!!)
|
private val favoritedRepository = FavoritedRepository(context!!)
|
||||||
|
|
||||||
override fun onDataFetched(data: List<Track>): List<Track> = runBlocking {
|
override fun onDataFetched(data: List<Track>): List<Track> = runBlocking {
|
||||||
val downloaded = TracksRepository.getDownloadedIds() ?: listOf()
|
val downloaded = TracksRepository.getDownloadedIds(exoDownloadManager) ?: listOf()
|
||||||
|
|
||||||
data.map { track ->
|
data.map { track ->
|
||||||
track.favorite = true
|
track.favorite = true
|
||||||
|
@ -55,7 +49,7 @@ class FavoritesRepository(override val context: Context?) : Repository<Track, Tr
|
||||||
|
|
||||||
track.bestUpload()?.let { upload ->
|
track.bestUpload()?.let { upload ->
|
||||||
maybeNormalizeUrl(upload.listen_url)?.let { url ->
|
maybeNormalizeUrl(upload.listen_url)?.let { url ->
|
||||||
track.cached = FFA.get().exoCache.isCached(url, 0, upload.duration * 1000L)
|
track.cached = exoCache.isCached(url, 0, upload.duration * 1000L)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,7 +63,7 @@ class FavoritesRepository(override val context: Context?) : Repository<Track, Tr
|
||||||
|
|
||||||
val request = Fuel.post(mustNormalizeUrl("/api/v1/favorites/tracks/")).apply {
|
val request = Fuel.post(mustNormalizeUrl("/api/v1/favorites/tracks/")).apply {
|
||||||
if (!Settings.isAnonymous()) {
|
if (!Settings.isAnonymous()) {
|
||||||
authorize(context)
|
authorize(context, oAuth)
|
||||||
header("Authorization", "Bearer ${oAuth.state().accessToken}")
|
header("Authorization", "Bearer ${oAuth.state().accessToken}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -91,7 +85,7 @@ class FavoritesRepository(override val context: Context?) : Repository<Track, Tr
|
||||||
|
|
||||||
val request = Fuel.post(mustNormalizeUrl("/api/v1/favorites/tracks/remove/")).apply {
|
val request = Fuel.post(mustNormalizeUrl("/api/v1/favorites/tracks/remove/")).apply {
|
||||||
if (!Settings.isAnonymous()) {
|
if (!Settings.isAnonymous()) {
|
||||||
authorize(context)
|
authorize(context, oAuth)
|
||||||
request.header("Authorization", "Bearer ${oAuth.state().accessToken}")
|
request.header("Authorization", "Bearer ${oAuth.state().accessToken}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -110,7 +104,7 @@ class FavoritesRepository(override val context: Context?) : Repository<Track, Tr
|
||||||
|
|
||||||
class FavoritedRepository(override val context: Context?) : Repository<Int, FavoritedCache>() {
|
class FavoritedRepository(override val context: Context?) : Repository<Int, FavoritedCache>() {
|
||||||
|
|
||||||
private val oAuth = OAuthFactory.instance()
|
private val oAuth: OAuth by inject(OAuth::class.java)
|
||||||
|
|
||||||
override val cacheId = "favorited"
|
override val cacheId = "favorited"
|
||||||
override val upstream = HttpUpstream<Int, OtterResponse<Int>>(
|
override val upstream = HttpUpstream<Int, OtterResponse<Int>>(
|
||||||
|
@ -127,7 +121,7 @@ class FavoritedRepository(override val context: Context?) : Repository<Int, Favo
|
||||||
|
|
||||||
fun update(context: Context?, scope: CoroutineScope) {
|
fun update(context: Context?, scope: CoroutineScope) {
|
||||||
fetch(Origin.Network.origin).untilNetwork(scope, IO) { favorites, _, _, _ ->
|
fetch(Origin.Network.origin).untilNetwork(scope, IO) { favorites, _, _, _ ->
|
||||||
Cache.set(context, cacheId, Gson().toJson(cache(favorites)).toByteArray())
|
FFACache.set(context, cacheId, Gson().toJson(cache(favorites)).toByteArray())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,8 +32,6 @@ class HttpUpstream<D : Any, R : OtterResponse<D>>(
|
||||||
Progressive
|
Progressive
|
||||||
}
|
}
|
||||||
|
|
||||||
private val http = HTTP(context)
|
|
||||||
|
|
||||||
override fun fetch(size: Int): Flow<Repository.Response<D>> = flow<Repository.Response<D>> {
|
override fun fetch(size: Int): Flow<Repository.Response<D>> = flow<Repository.Response<D>> {
|
||||||
|
|
||||||
context?.let {
|
context?.let {
|
||||||
|
@ -89,7 +87,7 @@ class HttpUpstream<D : Any, R : OtterResponse<D>>(
|
||||||
suspend fun get(context: Context, url: String): Result<R, FuelError> {
|
suspend fun get(context: Context, url: String): Result<R, FuelError> {
|
||||||
return try {
|
return try {
|
||||||
val request = Fuel.get(mustNormalizeUrl(url)).apply {
|
val request = Fuel.get(mustNormalizeUrl(url)).apply {
|
||||||
authorize(context)
|
authorize(context, oAuth)
|
||||||
}
|
}
|
||||||
val (_, _, result) = request.awaitObjectResponseResult(GenericDeserializer<R>(type))
|
val (_, _, result) = request.awaitObjectResponseResult(GenericDeserializer<R>(type))
|
||||||
result
|
result
|
||||||
|
|
|
@ -1,22 +1,19 @@
|
||||||
package audio.funkwhale.ffa.repositories
|
package audio.funkwhale.ffa.repositories
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import audio.funkwhale.ffa.utils.OAuthFactory
|
import audio.funkwhale.ffa.utils.*
|
||||||
import audio.funkwhale.ffa.utils.OtterResponse
|
|
||||||
import audio.funkwhale.ffa.utils.PlaylistTrack
|
|
||||||
import audio.funkwhale.ffa.utils.PlaylistTracksCache
|
|
||||||
import audio.funkwhale.ffa.utils.PlaylistTracksResponse
|
|
||||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.toList
|
import kotlinx.coroutines.flow.toList
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.koin.java.KoinJavaComponent.inject
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
|
|
||||||
class PlaylistTracksRepository(override val context: Context?, playlistId: Int) :
|
class PlaylistTracksRepository(override val context: Context?, playlistId: Int) :
|
||||||
Repository<PlaylistTrack, PlaylistTracksCache>() {
|
Repository<PlaylistTrack, PlaylistTracksCache>() {
|
||||||
|
|
||||||
private val oAuth = OAuthFactory.instance()
|
private val oAuth: OAuth by inject(OAuth::class.java)
|
||||||
|
|
||||||
override val cacheId = "tracks-playlist-$playlistId"
|
override val cacheId = "tracks-playlist-$playlistId"
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,7 @@
|
||||||
package audio.funkwhale.ffa.repositories
|
package audio.funkwhale.ffa.repositories
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import audio.funkwhale.ffa.utils.OAuthFactory
|
import audio.funkwhale.ffa.utils.*
|
||||||
import audio.funkwhale.ffa.utils.OtterResponse
|
|
||||||
import audio.funkwhale.ffa.utils.Playlist
|
|
||||||
import audio.funkwhale.ffa.utils.PlaylistsCache
|
|
||||||
import audio.funkwhale.ffa.utils.PlaylistsResponse
|
|
||||||
import audio.funkwhale.ffa.utils.Settings
|
|
||||||
import audio.funkwhale.ffa.utils.Track
|
|
||||||
import audio.funkwhale.ffa.utils.authorize
|
|
||||||
import audio.funkwhale.ffa.utils.mustNormalizeUrl
|
|
||||||
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
|
||||||
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
|
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
|
||||||
|
@ -18,6 +10,7 @@ import com.google.gson.Gson
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.java.KoinJavaComponent.inject
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
|
|
||||||
data class PlaylistAdd(val tracks: List<Int>, val allow_duplicates: Boolean)
|
data class PlaylistAdd(val tracks: List<Int>, val allow_duplicates: Boolean)
|
||||||
|
@ -26,12 +19,14 @@ class PlaylistsRepository(override val context: Context?) : Repository<Playlist,
|
||||||
|
|
||||||
override val cacheId = "tracks-playlists"
|
override val cacheId = "tracks-playlists"
|
||||||
|
|
||||||
|
private val oAuth: OAuth by inject(OAuth::class.java)
|
||||||
|
|
||||||
override val upstream = HttpUpstream<Playlist, OtterResponse<Playlist>>(
|
override val upstream = HttpUpstream<Playlist, OtterResponse<Playlist>>(
|
||||||
context!!,
|
context!!,
|
||||||
HttpUpstream.Behavior.Progressive,
|
HttpUpstream.Behavior.Progressive,
|
||||||
"/api/v1/playlists/?playable=true&ordering=name",
|
"/api/v1/playlists/?playable=true&ordering=name",
|
||||||
object : TypeToken<PlaylistsResponse>() {}.type,
|
object : TypeToken<PlaylistsResponse>() {}.type,
|
||||||
OAuthFactory.instance()
|
oAuth
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun cache(data: List<Playlist>) = PlaylistsCache(data)
|
override fun cache(data: List<Playlist>) = PlaylistsCache(data)
|
||||||
|
@ -42,7 +37,7 @@ class PlaylistsRepository(override val context: Context?) : Repository<Playlist,
|
||||||
class ManagementPlaylistsRepository(override val context: Context?) :
|
class ManagementPlaylistsRepository(override val context: Context?) :
|
||||||
Repository<Playlist, PlaylistsCache>() {
|
Repository<Playlist, PlaylistsCache>() {
|
||||||
|
|
||||||
private val oAuth = OAuthFactory.instance()
|
private val oAuth: OAuth by inject(OAuth::class.java)
|
||||||
|
|
||||||
override val cacheId = "tracks-playlists-management"
|
override val cacheId = "tracks-playlists-management"
|
||||||
|
|
||||||
|
@ -65,7 +60,7 @@ class ManagementPlaylistsRepository(override val context: Context?) :
|
||||||
|
|
||||||
val request = Fuel.post(mustNormalizeUrl("/api/v1/playlists/")).apply {
|
val request = Fuel.post(mustNormalizeUrl("/api/v1/playlists/")).apply {
|
||||||
if (!Settings.isAnonymous()) {
|
if (!Settings.isAnonymous()) {
|
||||||
authorize(context)
|
authorize(context, oAuth)
|
||||||
header("Authorization", "Bearer ${oAuth.state().accessToken}")
|
header("Authorization", "Bearer ${oAuth.state().accessToken}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,7 +83,7 @@ class ManagementPlaylistsRepository(override val context: Context?) :
|
||||||
|
|
||||||
val request = Fuel.post(mustNormalizeUrl("/api/v1/playlists/$id/add/")).apply {
|
val request = Fuel.post(mustNormalizeUrl("/api/v1/playlists/$id/add/")).apply {
|
||||||
if (!Settings.isAnonymous()) {
|
if (!Settings.isAnonymous()) {
|
||||||
authorize(context)
|
authorize(context, oAuth)
|
||||||
header("Authorization", "Bearer ${oAuth.state().accessToken}")
|
header("Authorization", "Bearer ${oAuth.state().accessToken}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -109,7 +104,7 @@ class ManagementPlaylistsRepository(override val context: Context?) :
|
||||||
|
|
||||||
val request = Fuel.post(mustNormalizeUrl("/api/v1/playlists/$id/remove/")).apply {
|
val request = Fuel.post(mustNormalizeUrl("/api/v1/playlists/$id/remove/")).apply {
|
||||||
if (!Settings.isAnonymous()) {
|
if (!Settings.isAnonymous()) {
|
||||||
authorize(context)
|
authorize(context, oAuth)
|
||||||
header("Authorization", "Bearer ${oAuth.state().accessToken}")
|
header("Authorization", "Bearer ${oAuth.state().accessToken}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -128,7 +123,7 @@ class ManagementPlaylistsRepository(override val context: Context?) :
|
||||||
|
|
||||||
val request = Fuel.post(mustNormalizeUrl("/api/v1/playlists/$id/move/")).apply {
|
val request = Fuel.post(mustNormalizeUrl("/api/v1/playlists/$id/move/")).apply {
|
||||||
if (!Settings.isAnonymous()) {
|
if (!Settings.isAnonymous()) {
|
||||||
authorize(context)
|
authorize(context, oAuth)
|
||||||
header("Authorization", "Bearer ${oAuth.state().accessToken}")
|
header("Authorization", "Bearer ${oAuth.state().accessToken}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,15 @@
|
||||||
package audio.funkwhale.ffa.repositories
|
package audio.funkwhale.ffa.repositories
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import audio.funkwhale.ffa.utils.OAuthFactory
|
import audio.funkwhale.ffa.utils.*
|
||||||
import audio.funkwhale.ffa.utils.OtterResponse
|
|
||||||
import audio.funkwhale.ffa.utils.Radio
|
|
||||||
import audio.funkwhale.ffa.utils.RadiosCache
|
|
||||||
import audio.funkwhale.ffa.utils.RadiosResponse
|
|
||||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
|
import org.koin.java.KoinJavaComponent.inject
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
|
|
||||||
class RadiosRepository(override val context: Context?) : Repository<Radio, RadiosCache>() {
|
class RadiosRepository(override val context: Context?) : Repository<Radio, RadiosCache>() {
|
||||||
|
|
||||||
private val oAuth = OAuthFactory.instance()
|
private val oAuth: OAuth by inject(OAuth::class.java)
|
||||||
|
|
||||||
override val cacheId = "radios"
|
override val cacheId = "radios"
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,8 @@ package audio.funkwhale.ffa.repositories
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import audio.funkwhale.ffa.utils.AppContext
|
import audio.funkwhale.ffa.utils.AppContext
|
||||||
import audio.funkwhale.ffa.utils.Cache
|
|
||||||
import audio.funkwhale.ffa.utils.CacheItem
|
import audio.funkwhale.ffa.utils.CacheItem
|
||||||
|
import audio.funkwhale.ffa.utils.FFACache
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
@ -32,16 +32,26 @@ abstract class Repository<D : Any, C : CacheItem<D>> {
|
||||||
open fun cache(data: List<D>): C? = null
|
open fun cache(data: List<D>): C? = null
|
||||||
protected open fun uncache(reader: BufferedReader): C? = null
|
protected open fun uncache(reader: BufferedReader): C? = null
|
||||||
|
|
||||||
fun fetch(upstreams: Int = Origin.Cache.origin and Origin.Network.origin, size: Int = 0): Flow<Response<D>> = flow {
|
fun fetch(
|
||||||
|
upstreams: Int = Origin.Cache.origin and Origin.Network.origin,
|
||||||
|
size: Int = 0
|
||||||
|
): Flow<Response<D>> = flow {
|
||||||
if (Origin.Cache.origin and upstreams == upstreams) fromCache().collect { emit(it) }
|
if (Origin.Cache.origin and upstreams == upstreams) fromCache().collect { emit(it) }
|
||||||
if (Origin.Network.origin and upstreams == upstreams) fromNetwork(size).collect { emit(it) }
|
if (Origin.Network.origin and upstreams == upstreams) fromNetwork(size).collect { emit(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fromCache() = flow {
|
private fun fromCache() = flow {
|
||||||
cacheId?.let { cacheId ->
|
cacheId?.let { cacheId ->
|
||||||
Cache.get(context, cacheId)?.let { reader ->
|
FFACache.get(context, cacheId)?.let { reader ->
|
||||||
uncache(reader)?.let { cache ->
|
uncache(reader)?.let { cache ->
|
||||||
return@flow emit(Response(Origin.Cache, cache.data, ceil(cache.data.size / AppContext.PAGE_SIZE.toDouble()).toInt(), false))
|
return@flow emit(
|
||||||
|
Response(
|
||||||
|
Origin.Cache,
|
||||||
|
cache.data,
|
||||||
|
ceil(cache.data.size / AppContext.PAGE_SIZE.toDouble()).toInt(),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,8 +62,24 @@ abstract class Repository<D : Any, C : CacheItem<D>> {
|
||||||
private fun fromNetwork(size: Int) = flow {
|
private fun fromNetwork(size: Int) = flow {
|
||||||
upstream
|
upstream
|
||||||
.fetch(size)
|
.fetch(size)
|
||||||
.map { response -> Response(Origin.Network, onDataFetched(response.data), response.page, response.hasMore) }
|
.map { response ->
|
||||||
.collect { response -> emit(Response(Origin.Network, response.data, response.page, response.hasMore)) }
|
Response(
|
||||||
|
Origin.Network,
|
||||||
|
onDataFetched(response.data),
|
||||||
|
response.page,
|
||||||
|
response.hasMore
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.collect { response ->
|
||||||
|
emit(
|
||||||
|
Response(
|
||||||
|
Origin.Network,
|
||||||
|
response.data,
|
||||||
|
response.page,
|
||||||
|
response.hasMore
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun onDataFetched(data: List<D>) = data
|
protected open fun onDataFetched(data: List<D>) = data
|
||||||
|
|
|
@ -1,29 +1,25 @@
|
||||||
package audio.funkwhale.ffa.repositories
|
package audio.funkwhale.ffa.repositories
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import audio.funkwhale.ffa.FFA
|
import audio.funkwhale.ffa.utils.*
|
||||||
import audio.funkwhale.ffa.utils.Album
|
|
||||||
import audio.funkwhale.ffa.utils.AlbumsCache
|
|
||||||
import audio.funkwhale.ffa.utils.AlbumsResponse
|
|
||||||
import audio.funkwhale.ffa.utils.Artist
|
|
||||||
import audio.funkwhale.ffa.utils.ArtistsCache
|
|
||||||
import audio.funkwhale.ffa.utils.ArtistsResponse
|
|
||||||
import audio.funkwhale.ffa.utils.OAuthFactory
|
|
||||||
import audio.funkwhale.ffa.utils.Track
|
|
||||||
import audio.funkwhale.ffa.utils.TracksCache
|
|
||||||
import audio.funkwhale.ffa.utils.TracksResponse
|
|
||||||
import audio.funkwhale.ffa.utils.mustNormalizeUrl
|
|
||||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||||
|
import com.google.android.exoplayer2.offline.DownloadManager
|
||||||
|
import com.google.android.exoplayer2.upstream.cache.Cache
|
||||||
|
import com.google.android.exoplayer2.upstream.cache.SimpleCache
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.toList
|
import kotlinx.coroutines.flow.toList
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.koin.core.qualifier.named
|
||||||
|
import org.koin.java.KoinJavaComponent.inject
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
|
|
||||||
class TracksSearchRepository(override val context: Context?, var query: String) :
|
class TracksSearchRepository(override val context: Context?, var query: String) :
|
||||||
Repository<Track, TracksCache>() {
|
Repository<Track, TracksCache>() {
|
||||||
|
|
||||||
private val oAuth = OAuthFactory.instance()
|
private val exoCache: Cache by inject(Cache::class.java, named("exoCache"))
|
||||||
|
private val exoDownloadManager: DownloadManager by inject(DownloadManager::class.java)
|
||||||
|
private val oAuth: OAuth by inject(OAuth::class.java)
|
||||||
|
|
||||||
override val cacheId: String? = null
|
override val cacheId: String? = null
|
||||||
|
|
||||||
|
@ -46,7 +42,7 @@ class TracksSearchRepository(override val context: Context?, var query: String)
|
||||||
.toList()
|
.toList()
|
||||||
.flatten()
|
.flatten()
|
||||||
|
|
||||||
val downloaded = TracksRepository.getDownloadedIds() ?: listOf()
|
val downloaded = TracksRepository.getDownloadedIds(exoDownloadManager) ?: listOf()
|
||||||
|
|
||||||
data.map { track ->
|
data.map { track ->
|
||||||
track.favorite = favorites.contains(track.id)
|
track.favorite = favorites.contains(track.id)
|
||||||
|
@ -55,7 +51,7 @@ class TracksSearchRepository(override val context: Context?, var query: String)
|
||||||
track.bestUpload()?.let { upload ->
|
track.bestUpload()?.let { upload ->
|
||||||
val url = mustNormalizeUrl(upload.listen_url)
|
val url = mustNormalizeUrl(upload.listen_url)
|
||||||
|
|
||||||
track.cached = FFA.get().exoCache.isCached(url, 0, upload.duration * 1000L)
|
track.cached = exoCache.isCached(url, 0, upload.duration * 1000L)
|
||||||
}
|
}
|
||||||
|
|
||||||
track
|
track
|
||||||
|
@ -66,7 +62,7 @@ class TracksSearchRepository(override val context: Context?, var query: String)
|
||||||
class ArtistsSearchRepository(override val context: Context?, var query: String) :
|
class ArtistsSearchRepository(override val context: Context?, var query: String) :
|
||||||
Repository<Artist, ArtistsCache>() {
|
Repository<Artist, ArtistsCache>() {
|
||||||
|
|
||||||
private val oAuth = OAuthFactory.instance()
|
private val oAuth: OAuth by inject(OAuth::class.java)
|
||||||
|
|
||||||
override val cacheId: String? = null
|
override val cacheId: String? = null
|
||||||
override val upstream: Upstream<Artist>
|
override val upstream: Upstream<Artist>
|
||||||
|
@ -86,7 +82,7 @@ class ArtistsSearchRepository(override val context: Context?, var query: String)
|
||||||
class AlbumsSearchRepository(override val context: Context?, var query: String) :
|
class AlbumsSearchRepository(override val context: Context?, var query: String) :
|
||||||
Repository<Album, AlbumsCache>() {
|
Repository<Album, AlbumsCache>() {
|
||||||
|
|
||||||
private val oAuth = OAuthFactory.instance()
|
private val oAuth: OAuth by inject(OAuth::class.java)
|
||||||
|
|
||||||
override val cacheId: String? = null
|
override val cacheId: String? = null
|
||||||
override val upstream: Upstream<Album>
|
override val upstream: Upstream<Album>
|
||||||
|
|
|
@ -1,26 +1,26 @@
|
||||||
package audio.funkwhale.ffa.repositories
|
package audio.funkwhale.ffa.repositories
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import audio.funkwhale.ffa.FFA
|
import audio.funkwhale.ffa.utils.*
|
||||||
import audio.funkwhale.ffa.utils.OAuthFactory
|
|
||||||
import audio.funkwhale.ffa.utils.OtterResponse
|
|
||||||
import audio.funkwhale.ffa.utils.Track
|
|
||||||
import audio.funkwhale.ffa.utils.TracksCache
|
|
||||||
import audio.funkwhale.ffa.utils.TracksResponse
|
|
||||||
import audio.funkwhale.ffa.utils.getMetadata
|
|
||||||
import audio.funkwhale.ffa.utils.mustNormalizeUrl
|
|
||||||
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
|
||||||
|
import com.google.android.exoplayer2.offline.DownloadManager
|
||||||
|
import com.google.android.exoplayer2.upstream.cache.Cache
|
||||||
|
import com.google.android.exoplayer2.upstream.cache.SimpleCache
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.toList
|
import kotlinx.coroutines.flow.toList
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.koin.core.qualifier.named
|
||||||
|
import org.koin.java.KoinJavaComponent.inject
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
|
|
||||||
class TracksRepository(override val context: Context?, albumId: Int) :
|
class TracksRepository(override val context: Context?, albumId: Int) :
|
||||||
Repository<Track, TracksCache>() {
|
Repository<Track, TracksCache>() {
|
||||||
|
|
||||||
private val oAuth = OAuthFactory.instance()
|
private val exoCache: Cache by inject(Cache::class.java, named("exoCache"))
|
||||||
|
private val oAuth: OAuth by inject(OAuth::class.java)
|
||||||
|
private val exoDownloadManager: DownloadManager by inject(DownloadManager::class.java)
|
||||||
|
|
||||||
override val cacheId = "tracks-album-$albumId"
|
override val cacheId = "tracks-album-$albumId"
|
||||||
|
|
||||||
|
@ -37,8 +37,8 @@ class TracksRepository(override val context: Context?, albumId: Int) :
|
||||||
gsonDeserializerOf(TracksCache::class.java).deserialize(reader)
|
gsonDeserializerOf(TracksCache::class.java).deserialize(reader)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getDownloadedIds(): List<Int>? {
|
fun getDownloadedIds(exoDownloadManager: DownloadManager): List<Int>? {
|
||||||
val cursor = FFA.get().exoDownloadManager.downloadIndex.getDownloads()
|
val cursor = exoDownloadManager.downloadIndex.getDownloads()
|
||||||
val ids: MutableList<Int> = mutableListOf()
|
val ids: MutableList<Int> = mutableListOf()
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
|
@ -61,7 +61,7 @@ class TracksRepository(override val context: Context?, albumId: Int) :
|
||||||
.toList()
|
.toList()
|
||||||
.flatten()
|
.flatten()
|
||||||
|
|
||||||
val downloaded = getDownloadedIds() ?: listOf()
|
val downloaded = getDownloadedIds(exoDownloadManager) ?: listOf()
|
||||||
|
|
||||||
data.map { track ->
|
data.map { track ->
|
||||||
track.favorite = favorites.contains(track.id)
|
track.favorite = favorites.contains(track.id)
|
||||||
|
@ -70,7 +70,7 @@ class TracksRepository(override val context: Context?, albumId: Int) :
|
||||||
track.bestUpload()?.let { upload ->
|
track.bestUpload()?.let { upload ->
|
||||||
val url = mustNormalizeUrl(upload.listen_url)
|
val url = mustNormalizeUrl(upload.listen_url)
|
||||||
|
|
||||||
track.cached = FFA.get().exoCache.isCached(url, 0, upload.duration * 1000L)
|
track.cached = exoCache.isCached(url, 0, upload.duration * 1000L)
|
||||||
}
|
}
|
||||||
|
|
||||||
track
|
track
|
||||||
|
|
|
@ -37,7 +37,7 @@ object AppContext {
|
||||||
cacheId = "$cacheId?$it"
|
cacheId = "$cacheId?$it"
|
||||||
}
|
}
|
||||||
|
|
||||||
Cache.set(context, cacheId, response.body().toByteArray())
|
FFACache.set(context, cacheId, response.body().toByteArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
next(request, response)
|
next(request, response)
|
||||||
|
|
|
@ -1,14 +1,6 @@
|
||||||
package audio.funkwhale.ffa.utils
|
package audio.funkwhale.ffa.utils
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import audio.funkwhale.ffa.activities.FwCredentials
|
|
||||||
import com.github.kittinunf.fuel.Fuel
|
|
||||||
import com.github.kittinunf.fuel.core.FuelError
|
|
||||||
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
|
|
||||||
import com.github.kittinunf.fuel.coroutines.awaitObjectResult
|
|
||||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
|
||||||
import com.github.kittinunf.result.Result
|
|
||||||
import com.preference.PowerPreference
|
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
|
@ -16,76 +8,7 @@ import java.security.MessageDigest
|
||||||
|
|
||||||
object RefreshError : Throwable()
|
object RefreshError : Throwable()
|
||||||
|
|
||||||
class HTTP(val context: Context?) {
|
object FFACache {
|
||||||
|
|
||||||
suspend fun refresh(): Boolean {
|
|
||||||
context?.let {
|
|
||||||
val body = mapOf(
|
|
||||||
"username" to PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS)
|
|
||||||
.getString("username"),
|
|
||||||
"password" to PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS)
|
|
||||||
.getString("password")
|
|
||||||
).toList()
|
|
||||||
|
|
||||||
val result = Fuel.post(mustNormalizeUrl("/api/v1/token"), body).apply {
|
|
||||||
if (!Settings.isAnonymous()) {
|
|
||||||
authorize(it)
|
|
||||||
header("Authorization", "Bearer ${OAuthFactory.instance().state().accessToken}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.awaitObjectResult(gsonDeserializerOf(FwCredentials::class.java))
|
|
||||||
|
|
||||||
return result.fold(
|
|
||||||
{ data ->
|
|
||||||
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS)
|
|
||||||
.setString("access_token", data.token)
|
|
||||||
|
|
||||||
true
|
|
||||||
},
|
|
||||||
{ false }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
throw IllegalStateException("Illegal state: context is null")
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend inline fun <reified T : Any> get(url: String): Result<T, FuelError> {
|
|
||||||
|
|
||||||
context?.let {
|
|
||||||
val request = Fuel.get(mustNormalizeUrl(url)).apply {
|
|
||||||
if (!Settings.isAnonymous()) {
|
|
||||||
authorize(it)
|
|
||||||
header("Authorization", "Bearer ${OAuthFactory.instance().state().accessToken}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val (_, response, result) = request.awaitObjectResponseResult(gsonDeserializerOf(T::class.java))
|
|
||||||
|
|
||||||
if (response.statusCode == 401) {
|
|
||||||
return retryGet(url)
|
|
||||||
} else {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw IllegalStateException("Illegal state: context is null")
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend inline fun <reified T : Any> retryGet(
|
|
||||||
url: String
|
|
||||||
): Result<T, FuelError> {
|
|
||||||
context?.let {
|
|
||||||
val request = Fuel.get(mustNormalizeUrl(url)).apply {
|
|
||||||
if (!Settings.isAnonymous()) {
|
|
||||||
authorize(context)
|
|
||||||
header("Authorization", "Bearer ${OAuthFactory.instance().state().accessToken}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
request.awaitObjectResult(gsonDeserializerOf(T::class.java))
|
|
||||||
}
|
|
||||||
throw IllegalStateException("Illegal state: context is null")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object Cache {
|
|
||||||
private fun key(key: String): String {
|
private fun key(key: String): String {
|
||||||
val md = MessageDigest.getInstance("SHA-1")
|
val md = MessageDigest.getInstance("SHA-1")
|
||||||
val digest = md.digest(key.toByteArray(Charset.defaultCharset()))
|
val digest = md.digest(key.toByteArray(Charset.defaultCharset()))
|
||||||
|
|
|
@ -76,21 +76,20 @@ fun Picasso.maybeLoad(url: String?): RequestCreator {
|
||||||
else load(url)
|
else load(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Request.authorize(context: Context): Request {
|
fun Request.authorize(context: Context, oAuth: OAuth): Request {
|
||||||
return runBlocking {
|
return runBlocking {
|
||||||
this@authorize.apply {
|
this@authorize.apply {
|
||||||
if (!Settings.isAnonymous()) {
|
if (!Settings.isAnonymous()) {
|
||||||
val oauth = OAuthFactory.instance()
|
oAuth.state().let { state ->
|
||||||
oauth.state().let { state ->
|
|
||||||
val old = state.accessToken
|
val old = state.accessToken
|
||||||
val auth = ClientSecretPost(oauth.state().clientSecret)
|
val auth = ClientSecretPost(oAuth.state().clientSecret)
|
||||||
val done = CompletableDeferred<Boolean>()
|
val done = CompletableDeferred<Boolean>()
|
||||||
|
|
||||||
state.performActionWithFreshTokens(oauth.service(context), auth) { token, _, _ ->
|
state.performActionWithFreshTokens(oAuth.service(context), auth) { token, _, _ ->
|
||||||
if (token != old && token != null) {
|
if (token != old && token != null) {
|
||||||
state.save()
|
state.save()
|
||||||
}
|
}
|
||||||
header("Authorization", "Bearer ${oauth.state().accessToken}")
|
header("Authorization", "Bearer ${oAuth.state().accessToken}")
|
||||||
done.complete(true)
|
done.complete(true)
|
||||||
}
|
}
|
||||||
done.await()
|
done.await()
|
||||||
|
|
|
@ -51,17 +51,6 @@ interface OAuth {
|
||||||
fun service(context: Context): AuthorizationService
|
fun service(context: Context): AuthorizationService
|
||||||
}
|
}
|
||||||
|
|
||||||
object OAuthFactory {
|
|
||||||
|
|
||||||
private val oAuth: OAuth
|
|
||||||
|
|
||||||
init {
|
|
||||||
oAuth = DefaultOAuth(AuthorizationServiceFactory())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun instance() = oAuth
|
|
||||||
}
|
|
||||||
|
|
||||||
class AuthorizationServiceFactory {
|
class AuthorizationServiceFactory {
|
||||||
|
|
||||||
fun create(context: Context): AuthorizationService {
|
fun create(context: Context): AuthorizationService {
|
||||||
|
|
|
@ -9,12 +9,12 @@ import com.preference.PowerPreference
|
||||||
|
|
||||||
object Userinfo {
|
object Userinfo {
|
||||||
|
|
||||||
suspend fun get(context: Context): User? {
|
suspend fun get(context: Context, oAuth: OAuth): User? {
|
||||||
try {
|
try {
|
||||||
val hostname =
|
val hostname =
|
||||||
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("hostname")
|
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("hostname")
|
||||||
val (_, _, result) = Fuel.get("$hostname/api/v1/users/users/me/")
|
val (_, _, result) = Fuel.get("$hostname/api/v1/users/users/me/")
|
||||||
.authorize(context)
|
.authorize(context, oAuth)
|
||||||
.awaitObjectResponseResult(gsonDeserializerOf(User::class.java))
|
.awaitObjectResponseResult(gsonDeserializerOf(User::class.java))
|
||||||
|
|
||||||
return when (result) {
|
return when (result) {
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
package audio.funkwhale.ffa
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.preference.PowerPreference
|
||||||
|
import com.preference.Preference
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.mockkStatic
|
||||||
|
import io.mockk.verify
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TemporaryFolder
|
||||||
|
import strikt.api.expectThat
|
||||||
|
import strikt.assertions.isFalse
|
||||||
|
|
||||||
|
class FFATest {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val temporaryFolder = TemporaryFolder()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `deleteAllData() should clear credentials preferences`() {
|
||||||
|
mockkStatic(PowerPreference::class)
|
||||||
|
val preference = mockk<Preference>(relaxed = true)
|
||||||
|
every { PowerPreference.getFileByName("credentials") } returns preference
|
||||||
|
|
||||||
|
val context = mockk<Context>()
|
||||||
|
every { context.cacheDir } returns mockk(relaxed = true)
|
||||||
|
FFA().deleteAllData(context)
|
||||||
|
verify { preference.clear() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `deleteAllData() should delete cacheDir contents`() {
|
||||||
|
mockkStatic(PowerPreference::class)
|
||||||
|
every { PowerPreference.getFileByName("credentials") } returns mockk(relaxed = true)
|
||||||
|
|
||||||
|
val tempFile = temporaryFolder.newFile()
|
||||||
|
|
||||||
|
val context = mockk<Context>()
|
||||||
|
every { context.cacheDir } returns temporaryFolder.root
|
||||||
|
FFA().deleteAllData(context)
|
||||||
|
|
||||||
|
expectThat(tempFile.exists()).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `deleteAllData() should delete picasso cache`() {
|
||||||
|
mockkStatic(PowerPreference::class)
|
||||||
|
every { PowerPreference.getFileByName("credentials") } returns mockk(relaxed = true)
|
||||||
|
|
||||||
|
val picassoCache = temporaryFolder.newFolder("picasso-cache")
|
||||||
|
|
||||||
|
val context = mockk<Context>()
|
||||||
|
every { context.cacheDir } returns temporaryFolder.root
|
||||||
|
|
||||||
|
FFA().deleteAllData(context)
|
||||||
|
|
||||||
|
expectThat(picassoCache.exists()).isFalse()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package audio.funkwhale.ffa
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import com.preference.PowerPreference
|
||||||
|
import org.koin.android.ext.koin.androidContext
|
||||||
|
import org.koin.core.context.loadKoinModules
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
|
import org.koin.core.context.unloadKoinModules
|
||||||
|
import org.koin.core.module.Module
|
||||||
|
|
||||||
|
class KoinTestApp : Application() {
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
PowerPreference.init(this)
|
||||||
|
startKoin {
|
||||||
|
androidContext(this@KoinTestApp)
|
||||||
|
modules(emptyList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadModules(module: Module, block: () -> Unit) {
|
||||||
|
loadKoinModules(module)
|
||||||
|
block()
|
||||||
|
unloadKoinModules(module)
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,23 +4,98 @@ import android.content.Intent
|
||||||
import androidx.test.core.app.ActivityScenario
|
import androidx.test.core.app.ActivityScenario
|
||||||
import androidx.test.core.app.ApplicationProvider
|
import androidx.test.core.app.ApplicationProvider
|
||||||
import audio.funkwhale.ffa.FFA
|
import audio.funkwhale.ffa.FFA
|
||||||
|
import audio.funkwhale.ffa.KoinTestApp
|
||||||
|
import audio.funkwhale.ffa.utils.OAuth
|
||||||
|
import com.preference.PowerPreference
|
||||||
|
import com.preference.Preference
|
||||||
|
import io.mockk.*
|
||||||
|
import org.junit.After
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
import org.koin.core.context.stopKoin
|
||||||
|
import org.koin.dsl.module
|
||||||
import org.robolectric.RobolectricTestRunner
|
import org.robolectric.RobolectricTestRunner
|
||||||
import org.robolectric.Shadows
|
import org.robolectric.Shadows
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
import strikt.api.expectThat
|
import strikt.api.expectThat
|
||||||
import strikt.assertions.isEqualTo
|
import strikt.assertions.isEqualTo
|
||||||
|
|
||||||
@RunWith(RobolectricTestRunner::class)
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
@Config(application = KoinTestApp::class, sdk = [30])
|
||||||
class SplashActivityTest {
|
class SplashActivityTest {
|
||||||
|
|
||||||
|
private val app: KoinTestApp = ApplicationProvider.getApplicationContext()
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
stopKoin()
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `unauthorized and nonAnonymous request should redirect to LoginActivity`() {
|
fun `unauthorized and nonAnonymous request should redirect to LoginActivity`() {
|
||||||
|
|
||||||
|
mockkStatic(PowerPreference::class)
|
||||||
|
val preference = mockk<Preference> {
|
||||||
|
every { getBoolean("anonymous", false) } returns false
|
||||||
|
every { clear() } returns true
|
||||||
|
}
|
||||||
|
every { PowerPreference.getFileByName("credentials") } returns preference
|
||||||
|
|
||||||
|
val modules = module {
|
||||||
|
single<OAuth> {
|
||||||
|
mockk { every { isAuthorized(any()) } returns false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.loadModules(modules) {
|
||||||
val scenario = ActivityScenario.launch(SplashActivity::class.java)
|
val scenario = ActivityScenario.launch(SplashActivity::class.java)
|
||||||
scenario.onActivity { activity ->
|
scenario.onActivity { activity ->
|
||||||
val expectedIntent = Intent(activity, LoginActivity::class.java)
|
val expectedIntent = Intent(activity, LoginActivity::class.java)
|
||||||
val appContext = Shadows.shadowOf(ApplicationProvider.getApplicationContext<FFA>())
|
val appContext = Shadows.shadowOf(ApplicationProvider.getApplicationContext<FFA>())
|
||||||
expectThat(appContext.nextStartedActivity.component).isEqualTo(expectedIntent.component)
|
expectThat(appContext.nextStartedActivity.component).isEqualTo(expectedIntent.component)
|
||||||
|
verify { preference.clear() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `authorized request should redirect to MainActivity`() {
|
||||||
|
val modules = module {
|
||||||
|
single<OAuth> {
|
||||||
|
mockk { every { isAuthorized(any()) } returns true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app.loadModules(modules) {
|
||||||
|
val scenario = ActivityScenario.launch(SplashActivity::class.java)
|
||||||
|
scenario.onActivity { activity ->
|
||||||
|
val expectedIntent = Intent(activity, MainActivity::class.java)
|
||||||
|
val appContext = Shadows.shadowOf(ApplicationProvider.getApplicationContext<FFA>())
|
||||||
|
expectThat(appContext.nextStartedActivity.component).isEqualTo(expectedIntent.component)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `anonymous requests should redirect to MainActivity`() {
|
||||||
|
|
||||||
|
mockkStatic(PowerPreference::class)
|
||||||
|
val preference = mockk<Preference>() {
|
||||||
|
every { getBoolean("anonymous", false) } returns true
|
||||||
|
}
|
||||||
|
every { PowerPreference.getFileByName("credentials") } returns preference
|
||||||
|
|
||||||
|
val modules = module {
|
||||||
|
single<OAuth> {
|
||||||
|
mockk { every { isAuthorized(any()) } returns false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app.loadModules(modules) {
|
||||||
|
val scenario = ActivityScenario.launch(SplashActivity::class.java)
|
||||||
|
scenario.onActivity { activity ->
|
||||||
|
val expectedIntent = Intent(activity, MainActivity::class.java)
|
||||||
|
val appContext = Shadows.shadowOf(ApplicationProvider.getApplicationContext<FFA>())
|
||||||
|
expectThat(appContext.nextStartedActivity.component).isEqualTo(expectedIntent.component)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,4 +17,5 @@ object Versions {
|
||||||
const val strikt = "0.31.0"
|
const val strikt = "0.31.0"
|
||||||
const val androidXTest = "1.4.0"
|
const val androidXTest = "1.4.0"
|
||||||
const val robolectric = "4.6.1"
|
const val robolectric = "4.6.1"
|
||||||
|
const val koin = "3.1.2"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue