Browse Source

Merge branch 'housekeeping/7-di-using-coin' into 'develop'

#7: Add Koin as dependency injection library

See merge request funkwhale/funkwhale-android!58
enhancement/speed-up-pipelines
Ryan Harg 3 years ago
parent
commit
709d678565
  1. 4
      app/build.gradle.kts
  2. 57
      app/src/main/java/audio/funkwhale/ffa/FFA.kt
  3. 8
      app/src/main/java/audio/funkwhale/ffa/activities/DownloadsActivity.kt
  4. 7
      app/src/main/java/audio/funkwhale/ffa/activities/LoginActivity.kt
  5. 21
      app/src/main/java/audio/funkwhale/ffa/activities/MainActivity.kt
  6. 14
      app/src/main/java/audio/funkwhale/ffa/activities/SettingsActivity.kt
  7. 30
      app/src/main/java/audio/funkwhale/ffa/activities/SplashActivity.kt
  8. 2
      app/src/main/java/audio/funkwhale/ffa/fragments/AddToPlaylistDialog.kt
  9. 4
      app/src/main/java/audio/funkwhale/ffa/fragments/FFAFragment.kt
  10. 17
      app/src/main/java/audio/funkwhale/ffa/fragments/FavoritesFragment.kt
  11. 21
      app/src/main/java/audio/funkwhale/ffa/fragments/TracksFragment.kt
  12. 71
      app/src/main/java/audio/funkwhale/ffa/koin/Modules.kt
  13. 7
      app/src/main/java/audio/funkwhale/ffa/playback/MediaControlsManager.kt
  14. 18
      app/src/main/java/audio/funkwhale/ffa/playback/PinService.kt
  15. 32
      app/src/main/java/audio/funkwhale/ffa/playback/PlayerService.kt
  16. 116
      app/src/main/java/audio/funkwhale/ffa/playback/QueueManager.kt
  17. 59
      app/src/main/java/audio/funkwhale/ffa/playback/RadioPlayer.kt
  18. 5
      app/src/main/java/audio/funkwhale/ffa/repositories/AlbumsRepository.kt
  19. 9
      app/src/main/java/audio/funkwhale/ffa/repositories/ArtistTracksRepository.kt
  20. 9
      app/src/main/java/audio/funkwhale/ffa/repositories/ArtistsRepository.kt
  21. 36
      app/src/main/java/audio/funkwhale/ffa/repositories/FavoritesRepository.kt
  22. 4
      app/src/main/java/audio/funkwhale/ffa/repositories/HttpUpstream.kt
  23. 9
      app/src/main/java/audio/funkwhale/ffa/repositories/PlaylistTracksRepository.kt
  24. 25
      app/src/main/java/audio/funkwhale/ffa/repositories/PlaylistsRepository.kt
  25. 9
      app/src/main/java/audio/funkwhale/ffa/repositories/RadiosRepository.kt
  26. 38
      app/src/main/java/audio/funkwhale/ffa/repositories/Repository.kt
  27. 30
      app/src/main/java/audio/funkwhale/ffa/repositories/SearchRepository.kt
  28. 26
      app/src/main/java/audio/funkwhale/ffa/repositories/TracksRepository.kt
  29. 2
      app/src/main/java/audio/funkwhale/ffa/utils/AppContext.kt
  30. 79
      app/src/main/java/audio/funkwhale/ffa/utils/Data.kt
  31. 11
      app/src/main/java/audio/funkwhale/ffa/utils/Extensions.kt
  32. 11
      app/src/main/java/audio/funkwhale/ffa/utils/OAuth.kt
  33. 4
      app/src/main/java/audio/funkwhale/ffa/utils/Userinfo.kt
  34. 61
      app/src/test/java/audio/funkwhale/ffa/FFATest.kt
  35. 27
      app/src/test/java/audio/funkwhale/ffa/KoinTestApp.kt
  36. 85
      app/src/test/java/audio/funkwhale/ffa/activities/SplashActivityTest.kt
  37. 1
      buildSrc/src/main/java/Versions.kt

4
app/build.gradle.kts

@ -171,6 +171,10 @@ dependencies { @@ -171,6 +171,10 @@ dependencies {
implementation("com.google.android.exoplayer:exoplayer-ui:${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}") {
isTransitive = false
}

57
app/src/main/java/audio/funkwhale/ffa/FFA.kt

@ -1,25 +1,19 @@ @@ -1,25 +1,19 @@
package audio.funkwhale.ffa
import android.app.Application
import android.content.Context
import androidx.appcompat.app.AppCompatDelegate
import audio.funkwhale.ffa.playback.MediaSession
import audio.funkwhale.ffa.playback.QueueManager
import audio.funkwhale.ffa.koin.ffaModule
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 kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import org.koin.core.context.startKoin
import java.text.SimpleDateFormat
import java.util.*
class FFA : Application() {
companion object {
private var instance: FFA = FFA()
@ -33,39 +27,13 @@ class FFA : Application() { @@ -33,39 +27,13 @@ class FFA : Application() {
val requestBus: BroadcastChannel<Request> = BroadcastChannel(10)
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() {
super.onCreate()
startKoin {
modules(ffaModule(this@FFA))
}
defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler(CrashReportHandler())
@ -81,14 +49,13 @@ class FFA : Application() { @@ -81,14 +49,13 @@ class FFA : Application() {
}
}
fun deleteAllData() {
fun deleteAllData(context: Context) {
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).clear()
cacheDir.listFiles()?.forEach {
context.cacheDir.listFiles()?.forEach {
it.delete()
}
cacheDir.resolve("picasso-cache").deleteRecursively()
context.cacheDir.resolve("picasso-cache").deleteRecursively()
}
inner class CrashReportHandler : Thread.UncaughtExceptionHandler {
@ -107,7 +74,7 @@ class FFA : Application() { @@ -107,7 +74,7 @@ class FFA : Application() {
builder.appendLine(e.toString())
Cache.set(this@FFA, "crashdump", builder.toString().toByteArray())
FFACache.set(this@FFA, "crashdump", builder.toString().toByteArray())
}
}

8
app/src/main/java/audio/funkwhale/ffa/activities/DownloadsActivity.kt

@ -4,24 +4,26 @@ import android.os.Bundle @@ -4,24 +4,26 @@ import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import audio.funkwhale.ffa.FFA
import audio.funkwhale.ffa.adapters.DownloadsAdapter
import audio.funkwhale.ffa.databinding.ActivityDownloadsBinding
import audio.funkwhale.ffa.utils.Event
import audio.funkwhale.ffa.utils.EventBus
import audio.funkwhale.ffa.utils.getMetadata
import com.google.android.exoplayer2.offline.Download
import com.google.android.exoplayer2.offline.DownloadManager
import kotlinx.coroutines.Dispatchers.Default
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.java.KoinJavaComponent.inject
class DownloadsActivity : AppCompatActivity() {
private lateinit var adapter: DownloadsAdapter
private lateinit var binding: ActivityDownloadsBinding
private val exoDownloadManager: DownloadManager by inject(DownloadManager::class.java)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -63,7 +65,7 @@ class DownloadsActivity : AppCompatActivity() { @@ -63,7 +65,7 @@ class DownloadsActivity : AppCompatActivity() {
private fun refresh() {
lifecycleScope.launch(Main) {
val cursor = FFA.get().exoDownloadManager.downloadIndex.getDownloads()
val cursor = exoDownloadManager.downloadIndex.getDownloads()
adapter.downloads.clear()
@ -99,7 +101,7 @@ class DownloadsActivity : AppCompatActivity() { @@ -99,7 +101,7 @@ class DownloadsActivity : AppCompatActivity() {
}
private suspend fun refreshProgress() {
val cursor = FFA.get().exoDownloadManager.downloadIndex.getDownloads()
val cursor = exoDownloadManager.downloadIndex.getDownloads()
while (cursor.moveToNext()) {
val download = cursor.download

7
app/src/main/java/audio/funkwhale/ffa/activities/LoginActivity.kt

@ -13,7 +13,6 @@ import audio.funkwhale.ffa.databinding.ActivityLoginBinding @@ -13,7 +13,6 @@ import audio.funkwhale.ffa.databinding.ActivityLoginBinding
import audio.funkwhale.ffa.fragments.LoginDialog
import audio.funkwhale.ffa.utils.AppContext
import audio.funkwhale.ffa.utils.OAuth
import audio.funkwhale.ffa.utils.OAuthFactory
import audio.funkwhale.ffa.utils.Userinfo
import audio.funkwhale.ffa.utils.log
import com.github.kittinunf.fuel.Fuel
@ -23,19 +22,19 @@ import com.github.kittinunf.result.Result @@ -23,19 +22,19 @@ import com.github.kittinunf.result.Result
import com.preference.PowerPreference
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.launch
import org.koin.java.KoinJavaComponent.inject
data class FwCredentials(val token: String, val non_field_errors: List<String>?)
class LoginActivity : AppCompatActivity() {
private lateinit var binding: ActivityLoginBinding
private lateinit var oAuth: OAuth
private val oAuth: OAuth by inject(OAuth::class.java)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityLoginBinding.inflate(layoutInflater)
oAuth = OAuthFactory.instance()
setContentView(binding.root)
limitContainerWidth()
}
@ -53,7 +52,7 @@ class LoginActivity : AppCompatActivity() { @@ -53,7 +52,7 @@ class LoginActivity : AppCompatActivity() {
.setBoolean("anonymous", false)
lifecycleScope.launch(Main) {
Userinfo.get(this@LoginActivity)?.let {
Userinfo.get(this@LoginActivity, oAuth)?.let {
startActivity(Intent(this@LoginActivity, MainActivity::class.java))
return@launch finish()

21
app/src/main/java/audio/funkwhale/ffa/activities/MainActivity.kt

@ -47,6 +47,7 @@ import kotlinx.coroutines.Dispatchers.Main @@ -47,6 +47,7 @@ import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.java.KoinJavaComponent.inject
class MainActivity : AppCompatActivity() {
enum class ResultCode(val code: Int) {
@ -58,6 +59,7 @@ class MainActivity : AppCompatActivity() { @@ -58,6 +59,7 @@ class MainActivity : AppCompatActivity() {
private var menu: Menu? = null
private lateinit var binding: ActivityMainBinding
private val oAuth: OAuth by inject(OAuth::class.java)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -103,7 +105,7 @@ class MainActivity : AppCompatActivity() { @@ -103,7 +105,7 @@ class MainActivity : AppCompatActivity() {
CommandBus.send(Command.RefreshService)
lifecycleScope.launch(IO) {
Userinfo.get(this@MainActivity)
Userinfo.get(this@MainActivity, oAuth)
}
with(binding) {
@ -260,7 +262,7 @@ class MainActivity : AppCompatActivity() { @@ -260,7 +262,7 @@ class MainActivity : AppCompatActivity() {
if (resultCode == ResultCode.LOGOUT.code) {
Intent(this, LoginActivity::class.java).apply {
FFA.get().deleteAllData()
FFA.get().deleteAllData(this@MainActivity)
flags =
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
@ -299,8 +301,7 @@ class MainActivity : AppCompatActivity() { @@ -299,8 +301,7 @@ class MainActivity : AppCompatActivity() {
EventBus.get().collect { message ->
when (message) {
is Event.LogOut -> {
FFA.get().deleteAllData()
FFA.get().deleteAllData(this@MainActivity)
startActivity(Intent(this@MainActivity, LoginActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NO_HISTORY
})
@ -494,10 +495,10 @@ class MainActivity : AppCompatActivity() { @@ -494,10 +495,10 @@ class MainActivity : AppCompatActivity() {
}
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 {
val current = Cache.get(this@MainActivity, "repeat")?.readLine()?.toInt() ?: 0
val current = FFACache.get(this@MainActivity, "repeat")?.readLine()?.toInt() ?: 0
changeRepeatMode((current + 1) % 3)
}
@ -577,7 +578,7 @@ class MainActivity : AppCompatActivity() { @@ -577,7 +578,7 @@ class MainActivity : AppCompatActivity() {
when (index) {
// From no repeat to repeat all
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?.setColorFilter(
@ -593,7 +594,7 @@ class MainActivity : AppCompatActivity() { @@ -593,7 +594,7 @@ class MainActivity : AppCompatActivity() {
// From repeat all to repeat one
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?.setColorFilter(
@ -609,7 +610,7 @@ class MainActivity : AppCompatActivity() { @@ -609,7 +610,7 @@ class MainActivity : AppCompatActivity() {
// From repeat one to no repeat
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?.setColorFilter(
ContextCompat.getColor(
@ -631,7 +632,7 @@ class MainActivity : AppCompatActivity() { @@ -631,7 +632,7 @@ class MainActivity : AppCompatActivity() {
try {
Fuel
.post(mustNormalizeUrl("/api/v1/history/listenings/"))
.authorize(this@MainActivity)
.authorize(this@MainActivity, oAuth)
.header("Content-Type", "application/json")
.body(Gson().toJson(mapOf("track" to track.id)))
.awaitStringResponse()

14
app/src/main/java/audio/funkwhale/ffa/activities/SettingsActivity.kt

@ -1,10 +1,6 @@ @@ -1,10 +1,6 @@
package audio.funkwhale.ffa.activities
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.*
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
@ -17,9 +13,9 @@ import androidx.preference.SeekBarPreference @@ -17,9 +13,9 @@ import androidx.preference.SeekBarPreference
import audio.funkwhale.ffa.FFA
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.databinding.ActivitySettingsBinding
import audio.funkwhale.ffa.utils.Cache
import audio.funkwhale.ffa.utils.Command
import audio.funkwhale.ffa.utils.CommandBus
import audio.funkwhale.ffa.utils.FFACache
class SettingsActivity : AppCompatActivity() {
@ -67,7 +63,7 @@ class SettingsFragment : @@ -67,7 +63,7 @@ class SettingsFragment :
"crash" -> {
activity?.let { activity ->
(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))
Toast.makeText(
@ -87,9 +83,7 @@ class SettingsFragment : @@ -87,9 +83,7 @@ class SettingsFragment :
.setMessage(context.getString(R.string.logout_content))
.setPositiveButton(android.R.string.ok) { _, _ ->
CommandBus.send(Command.ClearQueue)
FFA.get().deleteAllData()
FFA.get().deleteAllData(context)
activity?.setResult(MainActivity.ResultCode.LOGOUT.code)
activity?.finish()
}

30
app/src/main/java/audio/funkwhale/ffa/activities/SplashActivity.kt

@ -5,33 +5,31 @@ import android.content.Intent @@ -5,33 +5,31 @@ import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import audio.funkwhale.ffa.FFA
import audio.funkwhale.ffa.utils.AppContext
import audio.funkwhale.ffa.utils.OAuth
import audio.funkwhale.ffa.utils.OAuthFactory
import audio.funkwhale.ffa.utils.Settings
import audio.funkwhale.ffa.utils.*
import org.koin.java.KoinJavaComponent.inject
class SplashActivity : AppCompatActivity() {
private lateinit var oAuth: OAuth
private val oAuth: OAuth by inject(OAuth::class.java)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
oAuth = OAuthFactory.instance()
getSharedPreferences(AppContext.PREFS_CREDENTIALS, Context.MODE_PRIVATE)
.apply {
when (oAuth.isAuthorized(this@SplashActivity) || Settings.isAnonymous()) {
true -> Intent(this@SplashActivity, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NO_ANIMATION
startActivity(this)
}
true -> Intent(this@SplashActivity, MainActivity::class.java)
.apply {
flags = Intent.FLAG_ACTIVITY_NO_ANIMATION
startActivity(this)
}
false -> Intent(this@SplashActivity, LoginActivity::class.java).apply {
FFA.get().deleteAllData()
flags = Intent.FLAG_ACTIVITY_NO_ANIMATION
startActivity(this)
}
false -> Intent(this@SplashActivity, LoginActivity::class.java)
.apply {
FFA.get().deleteAllData(this@SplashActivity)
flags = Intent.FLAG_ACTIVITY_NO_ANIMATION
startActivity(this)
}
}
}
}

2
app/src/main/java/audio/funkwhale/ffa/fragments/AddToPlaylistDialog.kt

@ -115,7 +115,7 @@ object AddToPlaylistDialog { @@ -115,7 +115,7 @@ object AddToPlaylistDialog {
lifecycleScope.launch(IO) {
try {
Cache.set(
FFACache.set(
context,
cacheId,
Gson().toJson(cache(adapter.data)).toByteArray()

4
app/src/main/java/audio/funkwhale/ffa/fragments/FFAFragment.kt

@ -142,7 +142,7 @@ abstract class FFAFragment<D : Any, A : FFAAdapter<D, *>>() : Fragment() { @@ -142,7 +142,7 @@ abstract class FFAFragment<D : Any, A : FFAAdapter<D, *>>() : Fragment() {
withContext(IO) {
try {
repository.cacheId?.let { cacheId ->
Cache.set(
FFACache.set(
context,
cacheId,
Gson().toJson(repository.cache(adapter.data)).toByteArray()
@ -168,7 +168,7 @@ abstract class FFAFragment<D : Any, A : FFAAdapter<D, *>>() : Fragment() { @@ -168,7 +168,7 @@ abstract class FFAFragment<D : Any, A : FFAAdapter<D, *>>() : Fragment() {
(repository.upstream as? HttpUpstream<*, *>)?.let { upstream ->
when (upstream.behavior) {
HttpUpstream.Behavior.Progressive -> if (!hasMore || !moreLoading) swiper?.isRefreshing =
HttpUpstream.Behavior.Progressive -> if (!hasMore || !moreLoading) swiper.isRefreshing =
false
HttpUpstream.Behavior.AtOnce -> if (!hasMore) swiper.isRefreshing = false
HttpUpstream.Behavior.Single -> if (!hasMore) swiper.isRefreshing = false

17
app/src/main/java/audio/funkwhale/ffa/fragments/FavoritesFragment.kt

@ -10,25 +10,20 @@ import audio.funkwhale.ffa.adapters.FavoritesAdapter @@ -10,25 +10,20 @@ import audio.funkwhale.ffa.adapters.FavoritesAdapter
import audio.funkwhale.ffa.databinding.FragmentFavoritesBinding
import audio.funkwhale.ffa.repositories.FavoritesRepository
import audio.funkwhale.ffa.repositories.TracksRepository
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.wait
import audio.funkwhale.ffa.utils.*
import com.google.android.exoplayer2.offline.Download
import com.google.android.exoplayer2.offline.DownloadManager
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.java.KoinJavaComponent.inject
class FavoritesFragment : FFAFragment<Track, FavoritesAdapter>() {
private val exoDownloadManager: DownloadManager by inject(DownloadManager::class.java)
private var _binding: FragmentFavoritesBinding? = null
private val binding get() = _binding!!
@ -95,7 +90,7 @@ class FavoritesFragment : FFAFragment<Track, FavoritesAdapter>() { @@ -95,7 +90,7 @@ class FavoritesFragment : FFAFragment<Track, FavoritesAdapter>() {
}
private suspend fun refreshDownloadedTracks() {
val downloaded = TracksRepository.getDownloadedIds() ?: listOf()
val downloaded = TracksRepository.getDownloadedIds(exoDownloadManager) ?: listOf()
withContext(Main) {
adapter.data = adapter.data.map {

21
app/src/main/java/audio/funkwhale/ffa/fragments/TracksFragment.kt

@ -17,21 +17,9 @@ import audio.funkwhale.ffa.databinding.FragmentTracksBinding @@ -17,21 +17,9 @@ import audio.funkwhale.ffa.databinding.FragmentTracksBinding
import audio.funkwhale.ffa.repositories.FavoritedRepository
import audio.funkwhale.ffa.repositories.FavoritesRepository
import audio.funkwhale.ffa.repositories.TracksRepository
import audio.funkwhale.ffa.utils.Album
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 audio.funkwhale.ffa.utils.*
import com.google.android.exoplayer2.offline.Download
import com.google.android.exoplayer2.offline.DownloadManager
import com.preference.PowerPreference
import com.squareup.picasso.Picasso
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
@ -40,9 +28,12 @@ import kotlinx.coroutines.Dispatchers.Main @@ -40,9 +28,12 @@ import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.java.KoinJavaComponent.inject
class TracksFragment : FFAFragment<Track, TracksAdapter>() {
private val exoDownloadManager: DownloadManager by inject(DownloadManager::class.java)
override val recycler: RecyclerView get() = binding.tracks
private var _binding: FragmentTracksBinding? = null
@ -252,7 +243,7 @@ class TracksFragment : FFAFragment<Track, TracksAdapter>() { @@ -252,7 +243,7 @@ class TracksFragment : FFAFragment<Track, TracksAdapter>() {
}
private suspend fun refreshDownloadedTracks() {
val downloaded = TracksRepository.getDownloadedIds() ?: listOf()
val downloaded = TracksRepository.getDownloadedIds(exoDownloadManager) ?: listOf()
withContext(Main) {
adapter.data = adapter.data.map {

71
app/src/main/java/audio/funkwhale/ffa/koin/Modules.kt

@ -0,0 +1,71 @@ @@ -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) }
}

7
app/src/main/java/audio/funkwhale/ffa/playback/MediaControlsManager.kt

@ -10,7 +10,6 @@ import androidx.core.app.NotificationCompat @@ -10,7 +10,6 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.media.app.NotificationCompat.MediaStyle
import androidx.media.session.MediaButtonReceiver
import audio.funkwhale.ffa.FFA
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.activities.MainActivity
import audio.funkwhale.ffa.utils.AppContext
@ -20,12 +19,16 @@ import com.squareup.picasso.Picasso @@ -20,12 +19,16 @@ import com.squareup.picasso.Picasso
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.Default
import kotlinx.coroutines.launch
import org.koin.java.KoinJavaComponent.inject
class MediaControlsManager(val context: Service, private val scope: CoroutineScope, private val mediaSession: MediaSessionCompat) {
companion object {
const val NOTIFICATION_ACTION_OPEN_QUEUE = 0
}
private val ffaMediaSession: MediaSession by inject(MediaSession::class.java)
private var notification: Notification? = null
fun updateNotification(track: Track?, playing: Boolean) {
@ -99,7 +102,7 @@ class MediaControlsManager(val context: Service, private val scope: CoroutineSco @@ -99,7 +102,7 @@ class MediaControlsManager(val context: Service, private val scope: CoroutineSco
}
}
FFA.get().mediaSession.connector.invalidateMediaSessionMetadata()
ffaMediaSession.connector.invalidateMediaSessionMetadata()
}
}
}

18
app/src/main/java/audio/funkwhale/ffa/playback/PinService.kt

@ -4,17 +4,8 @@ import android.app.Notification @@ -4,17 +4,8 @@ import android.app.Notification
import android.content.Context
import android.content.Intent
import android.net.Uri
import audio.funkwhale.ffa.FFA
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.utils.AppContext
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 audio.funkwhale.ffa.utils.*
import com.google.android.exoplayer2.offline.Download
import com.google.android.exoplayer2.offline.DownloadManager
import com.google.android.exoplayer2.offline.DownloadRequest
@ -27,10 +18,13 @@ import kotlinx.coroutines.Dispatchers.Main @@ -27,10 +18,13 @@ import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import java.util.Collections
import org.koin.java.KoinJavaComponent
import java.util.*
class PinService : DownloadService(AppContext.NOTIFICATION_DOWNLOADS) {
private val scope: CoroutineScope = CoroutineScope(Job() + Main)
private val exoDownloadManager: DownloadManager by KoinJavaComponent.inject(DownloadManager::class.java)
companion object {
fun download(context: Context, track: Track) {
@ -74,7 +68,7 @@ class PinService : DownloadService(AppContext.NOTIFICATION_DOWNLOADS) { @@ -74,7 +68,7 @@ class PinService : DownloadService(AppContext.NOTIFICATION_DOWNLOADS) {
return super.onStartCommand(intent, flags, startId)
}
override fun getDownloadManager() = FFA.get().exoDownloadManager.apply {
override fun getDownloadManager() = exoDownloadManager.apply {
addListener(DownloadListener())
}

32
app/src/main/java/audio/funkwhale/ffa/playback/PlayerService.kt

@ -15,7 +15,6 @@ import android.support.v4.media.MediaMetadataCompat @@ -15,7 +15,6 @@ import android.support.v4.media.MediaMetadataCompat
import android.view.KeyEvent
import androidx.core.app.NotificationManagerCompat
import androidx.media.session.MediaButtonReceiver
import audio.funkwhale.ffa.FFA
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.utils.*
import com.google.android.exoplayer2.C
@ -29,12 +28,15 @@ import kotlinx.coroutines.* @@ -29,12 +28,15 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.flow.collect
import org.koin.java.KoinJavaComponent.inject
class PlayerService : Service() {
companion object {
const val INITIAL_COMMAND_KEY = "start_command"
}
private val mediaSession: MediaSession by inject(MediaSession::class.java)
private var started = false
private val scope: CoroutineScope = CoroutineScope(Job() + Main)
@ -63,12 +65,12 @@ class PlayerService : Service() { @@ -63,12 +65,12 @@ class PlayerService : Service() {
when (key.keyCode) {
KeyEvent.KEYCODE_MEDIA_PLAY, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
if (hasAudioFocus(true)) MediaButtonReceiver.handleIntent(
FFA.get().mediaSession.session,
mediaSession.session,
intent
)
Unit
}
else -> MediaButtonReceiver.handleIntent(FFA.get().mediaSession.session, intent)
else -> MediaButtonReceiver.handleIntent(mediaSession.session, intent)
}
}
}
@ -108,7 +110,7 @@ class PlayerService : Service() { @@ -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 {
playWhenReady = false
@ -118,9 +120,9 @@ class PlayerService : Service() { @@ -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)
setMediaMetadataProvider {
@ -129,9 +131,9 @@ class PlayerService : Service() { @@ -129,9 +131,9 @@ class PlayerService : Service() {
}
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())
val (current, duration, percent) = getProgress(true)
@ -161,7 +163,7 @@ class PlayerService : Service() { @@ -161,7 +163,7 @@ class PlayerService : Service() {
if (!command.fromRadio) radioPlayer.stop()
queue.replace(command.queue)
player.prepare(queue.datasources, true, true)
player.prepare(queue.dataSources, true, true)
setPlaybackState(true)
@ -271,7 +273,7 @@ class PlayerService : Service() { @@ -271,7 +273,7 @@ class PlayerService : Service() {
setPlaybackState(false)
player.release()
FFA.get().mediaSession.active = false
mediaSession.active = false
super.onDestroy()
}
@ -280,11 +282,11 @@ class PlayerService : Service() { @@ -280,11 +282,11 @@ class PlayerService : Service() {
if (!state) {
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) {
player.prepare(queue.datasources)
player.prepare(queue.dataSources)
}
if (hasAudioFocus(state)) {
@ -309,7 +311,7 @@ class PlayerService : Service() { @@ -309,7 +311,7 @@ class PlayerService : Service() {
private fun skipToNextTrack() {
player.next()
Cache.set(this@PlayerService, "progress", "0".toByteArray())
FFACache.set(this@PlayerService, "progress", "0".toByteArray())
ProgressBus.send(0, 0, 0)
}
@ -468,7 +470,7 @@ class PlayerService : Service() { @@ -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()))
}
@ -486,7 +488,7 @@ class PlayerService : Service() { @@ -486,7 +488,7 @@ class PlayerService : Service() {
if (player.playWhenReady) {
queue.current++
player.prepare(queue.datasources, true, true)
player.prepare(queue.dataSources, true, true)
player.seekTo(queue.current, 0)
CommandBus.send(Command.RefreshTrack(queue.current()))

116
app/src/main/java/audio/funkwhale/ffa/playback/QueueManager.kt

@ -2,18 +2,8 @@ package audio.funkwhale.ffa.playback @@ -2,18 +2,8 @@ package audio.funkwhale.ffa.playback
import android.content.Context
import android.net.Uri
import audio.funkwhale.ffa.FFA
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.utils.Cache
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 audio.funkwhale.ffa.utils.*
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
import com.google.android.exoplayer2.source.ConcatenatingMediaSource
import com.google.android.exoplayer2.source.ProgressiveMediaSource
@ -22,51 +12,62 @@ import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory @@ -22,51 +12,62 @@ import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory
import com.google.android.exoplayer2.upstream.FileDataSource
import com.google.android.exoplayer2.upstream.cache.CacheDataSource
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory
import com.google.android.exoplayer2.upstream.cache.Cache
import com.google.android.exoplayer2.util.Util
import com.google.gson.Gson
import org.koin.java.KoinJavaComponent.inject
class CacheDataSourceFactoryProvider(
private val oAuth: OAuth,
private val exoCache: Cache,
private val exoDownloadCache: Cache
) {
fun create(context: Context): CacheDataSourceFactory {
val playbackCache =
CacheDataSourceFactory(exoCache, createDatasourceFactory(context, oAuth))
return CacheDataSourceFactory(
exoDownloadCache,
playbackCache,
FileDataSource.Factory(),
null,
CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,
null
)
}
class QueueManager(val context: Context) {
var metadata: MutableList<Track> = mutableListOf()
val datasources = ConcatenatingMediaSource()
var current = -1
companion object {
fun factory(context: Context): CacheDataSourceFactory {
private fun createDatasourceFactory(context: Context, oAuth: OAuth): DataSource.Factory {
val http = DefaultHttpDataSourceFactory(
Util.getUserAgent(context, context.getString(R.string.app_name))
)
return if (!Settings.isAnonymous()) {
OAuth2DatasourceFactory(context, http, oAuth)
} else {
http
}
}
}
val playbackCache =
CacheDataSourceFactory(FFA.get().exoCache, createDatasourceFactory(context))
class QueueManager(val context: Context) {
return CacheDataSourceFactory(
FFA.get().exoDownloadCache,
playbackCache,
FileDataSource.Factory(),
null,
CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,
null
)
}
private val cacheDataSourceFactoryProvider: CacheDataSourceFactoryProvider by inject(
CacheDataSourceFactoryProvider::class.java
)
private fun createDatasourceFactory(context: Context): DataSource.Factory {
val http = DefaultHttpDataSourceFactory(
Util.getUserAgent(context, context.getString(R.string.app_name))
)
return if (!Settings.isAnonymous()) {
OAuth2DatasourceFactory(context, http, OAuthFactory.instance())
} else {
http
}
}
}
var metadata: MutableList<Track> = mutableListOf()
val dataSources = ConcatenatingMediaSource()
var current = -1
init {
Cache.get(context, "queue")?.let { json ->
FFACache.get(context, "queue")?.let { json ->
gsonDeserializerOf(QueueCache::class.java).deserialize(json)?.let { cache ->
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 ?: "")
ProgressiveMediaSource.Factory(factory).setTag(track.title)
@ -75,13 +76,13 @@ class QueueManager(val context: Context) { @@ -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()
}
}
private fun persist() {
Cache.set(
FFACache.set(
context,
"queue",
Gson().toJson(QueueCache(metadata)).toByteArray()
@ -89,8 +90,7 @@ class QueueManager(val context: Context) { @@ -89,8 +90,7 @@ class QueueManager(val context: Context) {
}
fun replace(tracks: List<Track>) {
val factory = factory(context)
val factory = cacheDataSourceFactoryProvider.create(context)
val sources = tracks.map { track ->
val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "")
@ -98,8 +98,8 @@ class QueueManager(val context: Context) { @@ -98,8 +98,8 @@ class QueueManager(val context: Context) {
}
metadata = tracks.toMutableList()
datasources.clear()
datasources.addMediaSources(sources)
dataSources.clear()
dataSources.addMediaSources(sources)
persist()
@ -107,7 +107,7 @@ class QueueManager(val context: Context) { @@ -107,7 +107,7 @@ class QueueManager(val context: Context) {
}
fun append(tracks: List<Track>) {
val factory = factory(context)
val factory = cacheDataSourceFactoryProvider.create(context)
val missingTracks = tracks.filter { metadata.indexOf(it) == -1 }
val sources = missingTracks.map { track ->
@ -117,7 +117,7 @@ class QueueManager(val context: Context) { @@ -117,7 +117,7 @@ class QueueManager(val context: Context) {
}
metadata.addAll(tracks)
datasources.addMediaSources(sources)
dataSources.addMediaSources(sources)
persist()
@ -125,12 +125,12 @@ class QueueManager(val context: Context) { @@ -125,12 +125,12 @@ class QueueManager(val context: Context) {
}
fun insertNext(track: Track) {
val factory = factory(context)
val factory = cacheDataSourceFactoryProvider.create(context)
val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "")
if (metadata.indexOf(track) == -1) {
ProgressiveMediaSource.Factory(factory).createMediaSource(Uri.parse(url)).let {
datasources.addMediaSource(current + 1, it)
dataSources.addMediaSource(current + 1, it)
metadata.add(current + 1, track)
}
} else {
@ -148,7 +148,7 @@ class QueueManager(val context: Context) { @@ -148,7 +148,7 @@ class QueueManager(val context: Context) {
return
}
datasources.removeMediaSource(it)
dataSources.removeMediaSource(it)
metadata.removeAt(it)
if (it == current) {
@ -170,7 +170,7 @@ class QueueManager(val context: Context) { @@ -170,7 +170,7 @@ class QueueManager(val context: Context) {
}
fun move(oldPosition: Int, newPosition: Int) {
datasources.moveMediaSource(oldPosition, newPosition)
dataSources.moveMediaSource(oldPosition, newPosition)
metadata.add(newPosition, metadata.removeAt(oldPosition))
persist()
@ -193,7 +193,7 @@ class QueueManager(val context: Context) { @@ -193,7 +193,7 @@ class QueueManager(val context: Context) {
fun clear() {
metadata = mutableListOf()
datasources.clear()
dataSources.clear()
current = -1
persist()
@ -214,7 +214,7 @@ class QueueManager(val context: Context) { @@ -214,7 +214,7 @@ class QueueManager(val context: Context) {
.shuffled()
while (metadata.size > 1) {
datasources.removeMediaSource(metadata.size - 1)
dataSources.removeMediaSource(metadata.size - 1)
metadata.removeAt(metadata.size - 1)
}

59
app/src/main/java/audio/funkwhale/ffa/playback/RadioPlayer.kt

@ -4,16 +4,7 @@ import android.content.Context @@ -4,16 +4,7 @@ import android.content.Context
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.repositories.FavoritedRepository
import audio.funkwhale.ffa.repositories.Repository
import audio.funkwhale.ffa.utils.Cache
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 audio.funkwhale.ffa.utils.*
import com.github.kittinunf.fuel.Fuel
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
import com.github.kittinunf.fuel.coroutines.awaitObjectResult
@ -27,8 +18,14 @@ import kotlinx.coroutines.flow.toList @@ -27,8 +18,14 @@ import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore
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 RadioTrackBody(val session: Int)
data class RadioTrack(val position: Int, val track: RadioTrackID)
@ -37,6 +34,7 @@ data class RadioTrackID(val id: Int) @@ -37,6 +34,7 @@ data class RadioTrackID(val id: Int)
class RadioPlayer(val context: Context, val scope: CoroutineScope) {
val lock = Semaphore(1)
private val oAuth: OAuth by inject(OAuth::class.java)
private var currentRadio: Radio? = null
private var session: Int? = null
private var cookie: String? = null
@ -44,10 +42,10 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) { @@ -44,10 +42,10 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) {
private val favoritedRepository = FavoritedRepository(context)
init {
Cache.get(context, "radio_type")?.readLine()?.let { radio_type ->
Cache.get(context, "radio_id")?.readLine()?.toInt()?.let { radio_id ->
Cache.get(context, "radio_session")?.readLine()?.toInt()?.let { radio_session ->
val cachedCookie = Cache.get(context, "radio_cookie")?.readLine()
FFACache.get(context, "radio_type")?.readLine()?.let { radio_type ->
FFACache.get(context, "radio_id")?.readLine()?.toInt()?.let { radio_id ->
FFACache.get(context, "radio_session")?.readLine()?.toInt()?.let { radio_session ->
val cachedCookie = FFACache.get(context, "radio_cookie")?.readLine()
currentRadio = Radio(radio_id, radio_type, "", "")
session = radio_session
@ -70,10 +68,10 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) { @@ -70,10 +68,10 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) {
currentRadio = null
session = null
Cache.delete(context, "radio_type")
Cache.delete(context, "radio_id")
Cache.delete(context, "radio_session")
Cache.delete(context, "radio_cookie")
FFACache.delete(context, "radio_type")
FFACache.delete(context, "radio_id")
FFACache.delete(context, "radio_session")
FFACache.delete(context, "radio_cookie")
}
fun isActive() = currentRadio != null && session != null
@ -81,15 +79,16 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) { @@ -81,15 +79,16 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) {
private suspend fun createSession() {
currentRadio?.let { radio ->
try {
val request = RadioSessionBody(radio.radio_type, related_object_id = radio.related_object_id).apply {
if (radio_type == "custom") {
custom_radio = radio.id
val request =
RadioSessionBody(radio.radio_type, related_object_id = radio.related_object_id).apply {
if (radio_type == "custom") {
custom_radio = radio.id
}
}
}
val body = Gson().toJson(request)
val (_, response, result) = Fuel.post(mustNormalizeUrl("/api/v1/radios/sessions/"))
.authorize(context)
.authorize(context, oAuth)
.header("Content-Type", "application/json")
.body(body)
.awaitObjectResponseResult(gsonDeserializerOf(RadioSession::class.java))
@ -97,10 +96,10 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) { @@ -97,10 +96,10 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) {
session = result.get().id
cookie = response.header("set-cookie").joinToString(";")
Cache.set(context, "radio_type", radio.radio_type.toByteArray())
Cache.set(context, "radio_id", radio.id.toString().toByteArray())
Cache.set(context, "radio_session", session.toString().toByteArray())
Cache.set(context, "radio_cookie", cookie.toString().toByteArray())
FFACache.set(context, "radio_type", radio.radio_type.toByteArray())
FFACache.set(context, "radio_id", radio.id.toString().toByteArray())
FFACache.set(context, "radio_session", session.toString().toByteArray())
FFACache.set(context, "radio_cookie", cookie.toString().toByteArray())
prepareNextTrack(true)
} catch (e: Exception) {
@ -116,7 +115,7 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) { @@ -116,7 +115,7 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) {
try {
val body = Gson().toJson(RadioTrackBody(session))
val result = Fuel.post(mustNormalizeUrl("/api/v1/radios/tracks/"))
.authorize(context)
.authorize(context, oAuth)
.header("Content-Type", "application/json")
.apply {
cookie?.let {
@ -127,7 +126,7 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) { @@ -127,7 +126,7 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) {
.awaitObjectResult(gsonDeserializerOf(RadioTrack::class.java))
val trackResponse = Fuel.get(mustNormalizeUrl("/api/v1/tracks/${result.get().track.id}/"))
.authorize(context)
.authorize(context, oAuth)
.awaitObjectResult(gsonDeserializerOf(Track::class.java))
val favorites = favoritedRepository.fetch(Repository.Origin.Cache.origin)

5
app/src/main/java/audio/funkwhale/ffa/repositories/AlbumsRepository.kt

@ -4,15 +4,16 @@ import android.content.Context @@ -4,15 +4,16 @@ import android.content.Context
import audio.funkwhale.ffa.utils.Album
import audio.funkwhale.ffa.utils.AlbumsCache
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.google.gson.reflect.TypeToken
import org.koin.java.KoinJavaComponent.inject
import java.io.BufferedReader
class AlbumsRepository(override val context: Context?, artistId: Int? = null) :
Repository<Album, AlbumsCache>() {
private val oAuth = OAuthFactory.instance()
private val oAuth: OAuth by inject(OAuth::class.java)
override val cacheId: String by lazy {
if (artistId == null) "albums"

9
app/src/main/java/audio/funkwhale/ffa/repositories/ArtistTracksRepository.kt

@ -1,19 +1,16 @@ @@ -1,19 +1,16 @@
package audio.funkwhale.ffa.repositories
import android.content.Context
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.*
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
import com.google.gson.reflect.TypeToken
import org.koin.java.KoinJavaComponent.inject
import java.io.BufferedReader
class ArtistTracksRepository(override val context: Context?, private val artistId: Int) :
Repository<Track, TracksCache>() {
private val oAuth = OAuthFactory.instance()
private val oAuth: OAuth by inject(OAuth::class.java)
override val cacheId = "tracks-artist-$artistId"

9
app/src/main/java/audio/funkwhale/ffa/repositories/ArtistsRepository.kt

@ -1,18 +1,15 @@ @@ -1,18 +1,15 @@
package audio.funkwhale.ffa.repositories
import android.content.Context
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.OtterResponse
import audio.funkwhale.ffa.utils.*
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
import com.google.gson.reflect.TypeToken
import org.koin.java.KoinJavaComponent.inject
import java.io.BufferedReader
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"

36
app/src/main/java/audio/funkwhale/ffa/repositories/FavoritesRepository.kt

@ -1,34 +1,28 @@ @@ -1,34 +1,28 @@
package audio.funkwhale.ffa.repositories
import android.content.Context
import audio.funkwhale.ffa.FFA
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 audio.funkwhale.ffa.utils.*
import com.github.kittinunf.fuel.Fuel
import com.github.kittinunf.fuel.coroutines.awaitByteArrayResponseResult
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.reflect.TypeToken
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.koin.core.qualifier.named
import org.koin.java.KoinJavaComponent.inject
import java.io.BufferedReader
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"
@ -47,7 +41,7 @@ class FavoritesRepository(override val context: Context?) : Repository<Track, Tr @@ -47,7 +41,7 @@ class FavoritesRepository(override val context: Context?) : Repository<Track, Tr
private val favoritedRepository = FavoritedRepository(context!!)
override fun onDataFetched(data: List<Track>): List<Track> = runBlocking {
val downloaded = TracksRepository.getDownloadedIds() ?: listOf()
val downloaded = TracksRepository.getDownloadedIds(exoDownloadManager) ?: listOf()
data.map { track ->
track.favorite = true
@ -55,7 +49,7 @@ class FavoritesRepository(override val context: Context?) : Repository<Track, Tr @@ -55,7 +49,7 @@ class FavoritesRepository(override val context: Context?) : Repository<Track, Tr
track.bestUpload()?.let { upload ->
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 @@ -69,7 +63,7 @@ class FavoritesRepository(override val context: Context?) : Repository<Track, Tr
val request = Fuel.post(mustNormalizeUrl("/api/v1/favorites/tracks/")).apply {
if (!Settings.isAnonymous()) {
authorize(context)
authorize(context, oAuth)
header("Authorization", "Bearer ${oAuth.state().accessToken}")
}
}
@ -91,7 +85,7 @@ class FavoritesRepository(override val context: Context?) : Repository<Track, Tr @@ -91,7 +85,7 @@ class FavoritesRepository(override val context: Context?) : Repository<Track, Tr
val request = Fuel.post(mustNormalizeUrl("/api/v1/favorites/tracks/remove/")).apply {
if (!Settings.isAnonymous()) {
authorize(context)
authorize(context, oAuth)
request.header("Authorization", "Bearer ${oAuth.state().accessToken}")
}
}
@ -110,7 +104,7 @@ class FavoritesRepository(override val context: Context?) : Repository<Track, Tr @@ -110,7 +104,7 @@ class FavoritesRepository(override val context: Context?) : Repository<Track, Tr
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 upstream = HttpUpstream<Int, OtterResponse<Int>>(
@ -127,7 +121,7 @@ class FavoritedRepository(override val context: Context?) : Repository<Int, Favo @@ -127,7 +121,7 @@ class FavoritedRepository(override val context: Context?) : Repository<Int, Favo
fun update(context: Context?, scope: CoroutineScope) {
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())
}
}
}

4
app/src/main/java/audio/funkwhale/ffa/repositories/HttpUpstream.kt

@ -32,8 +32,6 @@ class HttpUpstream<D : Any, R : OtterResponse<D>>( @@ -32,8 +32,6 @@ class HttpUpstream<D : Any, R : OtterResponse<D>>(
Progressive
}
private val http = HTTP(context)
override fun fetch(size: Int): Flow<Repository.Response<D>> = flow<Repository.Response<D>> {
context?.let {
@ -89,7 +87,7 @@ class HttpUpstream<D : Any, R : OtterResponse<D>>( @@ -89,7 +87,7 @@ class HttpUpstream<D : Any, R : OtterResponse<D>>(
suspend fun get(context: Context, url: String): Result<R, FuelError> {
return try {
val request = Fuel.get(mustNormalizeUrl(url)).apply {
authorize(context)
authorize(context, oAuth)
}
val (_, _, result) = request.awaitObjectResponseResult(GenericDeserializer<R>(type))
result

9
app/src/main/java/audio/funkwhale/ffa/repositories/PlaylistTracksRepository.kt

@ -1,22 +1,19 @@ @@ -1,22 +1,19 @@
package audio.funkwhale.ffa.repositories
import android.content.Context
import audio.funkwhale.ffa.utils.OAuthFactory
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 audio.funkwhale.ffa.utils.*
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
import org.koin.java.KoinJavaComponent.inject
import java.io.BufferedReader
class PlaylistTracksRepository(override val context: Context?, playlistId: Int) :
Repository<PlaylistTrack, PlaylistTracksCache>() {
private val oAuth = OAuthFactory.instance()
private val oAuth: OAuth by inject(OAuth::class.java)
override val cacheId = "tracks-playlist-$playlistId"

25
app/src/main/java/audio/funkwhale/ffa/repositories/PlaylistsRepository.kt

@ -1,15 +1,7 @@ @@ -1,15 +1,7 @@
package audio.funkwhale.ffa.repositories
import android.content.Context
import audio.funkwhale.ffa.utils.OAuthFactory
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 audio.funkwhale.ffa.utils.*
import com.github.kittinunf.fuel.Fuel
import com.github.kittinunf.fuel.coroutines.awaitByteArrayResponseResult
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
@ -18,6 +10,7 @@ import com.google.gson.Gson @@ -18,6 +10,7 @@ import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.koin.java.KoinJavaComponent.inject
import java.io.BufferedReader
data class PlaylistAdd(val tracks: List<Int>, val allow_duplicates: Boolean)
@ -26,12 +19,14 @@ class PlaylistsRepository(override val context: Context?) : Repository<Playlist, @@ -26,12 +19,14 @@ class PlaylistsRepository(override val context: Context?) : Repository<Playlist,
override val cacheId = "tracks-playlists"
private val oAuth: OAuth by inject(OAuth::class.java)
override val upstream = HttpUpstream<Playlist, OtterResponse<Playlist>>(
context!!,
HttpUpstream.Behavior.Progressive,
"/api/v1/playlists/?playable=true&ordering=name",
object : TypeToken<PlaylistsResponse>() {}.type,
OAuthFactory.instance()
oAuth
)
override fun cache(data: List<Playlist>) = PlaylistsCache(data)
@ -42,7 +37,7 @@ class PlaylistsRepository(override val context: Context?) : Repository<Playlist, @@ -42,7 +37,7 @@ class PlaylistsRepository(override val context: Context?) : Repository<Playlist,
class ManagementPlaylistsRepository(override val context: Context?) :
Repository<Playlist, PlaylistsCache>() {
private val oAuth = OAuthFactory.instance()
private val oAuth: OAuth by inject(OAuth::class.java)
override val cacheId = "tracks-playlists-management"
@ -65,7 +60,7 @@ class ManagementPlaylistsRepository(override val context: Context?) : @@ -65,7 +60,7 @@ class ManagementPlaylistsRepository(override val context: Context?) :
val request = Fuel.post(mustNormalizeUrl("/api/v1/playlists/")).apply {
if (!Settings.isAnonymous()) {
authorize(context)
authorize(context, oAuth)
header("Authorization", "Bearer ${oAuth.state().accessToken}")
}
}
@ -88,7 +83,7 @@ class ManagementPlaylistsRepository(override val context: Context?) : @@ -88,7 +83,7 @@ class ManagementPlaylistsRepository(override val context: Context?) :
val request = Fuel.post(mustNormalizeUrl("/api/v1/playlists/$id/add/")).apply {
if (!Settings.isAnonymous()) {
authorize(context)
authorize(context, oAuth)
header("Authorization", "Bearer ${oAuth.state().accessToken}")
}
}
@ -109,7 +104,7 @@ class ManagementPlaylistsRepository(override val context: Context?) : @@ -109,7 +104,7 @@ class ManagementPlaylistsRepository(override val context: Context?) :
val request = Fuel.post(mustNormalizeUrl("/api/v1/playlists/$id/remove/")).apply {
if (!Settings.isAnonymous()) {
authorize(context)
authorize(context, oAuth)
header("Authorization", "Bearer ${oAuth.state().accessToken}")
}
}
@ -128,7 +123,7 @@ class ManagementPlaylistsRepository(override val context: Context?) : @@ -128,7 +123,7 @@ class ManagementPlaylistsRepository(override val context: Context?) :
val request = Fuel.post(mustNormalizeUrl("/api/v1/playlists/$id/move/")).apply {
if (!Settings.isAnonymous()) {
authorize(context)
authorize(context, oAuth)
header("Authorization", "Bearer ${oAuth.state().accessToken}")
}
}

9
app/src/main/java/audio/funkwhale/ffa/repositories/RadiosRepository.kt

@ -1,18 +1,15 @@ @@ -1,18 +1,15 @@
package audio.funkwhale.ffa.repositories
import android.content.Context
import audio.funkwhale.ffa.utils.OAuthFactory
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 audio.funkwhale.ffa.utils.*
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
import com.google.gson.reflect.TypeToken
import org.koin.java.KoinJavaComponent.inject
import java.io.BufferedReader
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"

38
app/src/main/java/audio/funkwhale/ffa/repositories/Repository.kt

@ -2,8 +2,8 @@ package audio.funkwhale.ffa.repositories @@ -2,8 +2,8 @@ package audio.funkwhale.ffa.repositories
import android.content.Context
import audio.funkwhale.ffa.utils.AppContext
import audio.funkwhale.ffa.utils.Cache
import audio.funkwhale.ffa.utils.CacheItem
import audio.funkwhale.ffa.utils.FFACache
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Job
@ -32,16 +32,26 @@ abstract class Repository<D : Any, C : CacheItem<D>> { @@ -32,16 +32,26 @@ abstract class Repository<D : Any, C : CacheItem<D>> {
open fun cache(data: List<D>): 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.Network.origin and upstreams == upstreams) fromNetwork(size).collect { emit(it) }
}
private fun fromCache() = flow {
cacheId?.let { cacheId ->
Cache.get(context, cacheId)?.let { reader ->
FFACache.get(context, cacheId)?.let { reader ->
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>> { @@ -52,8 +62,24 @@ abstract class Repository<D : Any, C : CacheItem<D>> {
private fun fromNetwork(size: Int) = flow {
upstream
.fetch(size)
.map { response -> Response(Origin.Network, onDataFetched(response.data), response.page, response.hasMore) }
.collect { response -> emit(Response(Origin.Network, response.data, response.page, response.hasMore)) }
.map { response ->
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

30
app/src/main/java/audio/funkwhale/ffa/repositories/SearchRepository.kt

@ -1,29 +1,25 @@ @@ -1,29 +1,25 @@
package audio.funkwhale.ffa.repositories
import android.content.Context
import audio.funkwhale.ffa.FFA
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 audio.funkwhale.ffa.utils.*
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 kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
import org.koin.core.qualifier.named
import org.koin.java.KoinJavaComponent.inject
import java.io.BufferedReader
class TracksSearchRepository(override val context: Context?, var query: String) :
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
@ -46,7 +42,7 @@ class TracksSearchRepository(override val context: Context?, var query: String) @@ -46,7 +42,7 @@ class TracksSearchRepository(override val context: Context?, var query: String)
.toList()
.flatten()
val downloaded = TracksRepository.getDownloadedIds() ?: listOf()
val downloaded = TracksRepository.getDownloadedIds(exoDownloadManager) ?: listOf()
data.map { track ->
track.favorite = favorites.contains(track.id)
@ -55,7 +51,7 @@ class TracksSearchRepository(override val context: Context?, var query: String) @@ -55,7 +51,7 @@ class TracksSearchRepository(override val context: Context?, var query: String)
track.bestUpload()?.let { upload ->
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
@ -66,7 +62,7 @@ class TracksSearchRepository(override val context: Context?, var query: String) @@ -66,7 +62,7 @@ class TracksSearchRepository(override val context: Context?, var query: String)
class ArtistsSearchRepository(override val context: Context?, var query: String) :
Repository<Artist, ArtistsCache>() {
private val oAuth = OAuthFactory.instance()
private val oAuth: OAuth by inject(OAuth::class.java)
override val cacheId: String? = null
override val upstream: Upstream<Artist>
@ -86,7 +82,7 @@ class ArtistsSearchRepository(override val context: Context?, var query: String) @@ -86,7 +82,7 @@ class ArtistsSearchRepository(override val context: Context?, var query: String)
class AlbumsSearchRepository(override val context: Context?, var query: String) :
Repository<Album, AlbumsCache>() {
private val oAuth = OAuthFactory.instance()
private val oAuth: OAuth by inject(OAuth::class.java)
override val cacheId: String? = null
override val upstream: Upstream<Album>

26
app/src/main/java/audio/funkwhale/ffa/repositories/TracksRepository.kt

@ -1,26 +1,26 @@ @@ -1,26 +1,26 @@
package audio.funkwhale.ffa.repositories
import android.content.Context
import audio.funkwhale.ffa.FFA
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 audio.funkwhale.ffa.utils.*
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
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 kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
import org.koin.core.qualifier.named
import org.koin.java.KoinJavaComponent.inject
import java.io.BufferedReader
class TracksRepository(override val context: Context?, albumId: Int) :
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"
@ -37,8 +37,8 @@ class TracksRepository(override val context: Context?, albumId: Int) : @@ -37,8 +37,8 @@ class TracksRepository(override val context: Context?, albumId: Int) :
gsonDeserializerOf(TracksCache::class.java).deserialize(reader)
companion object {
fun getDownloadedIds(): List<Int>? {
val cursor = FFA.get().exoDownloadManager.downloadIndex.getDownloads()
fun getDownloadedIds(exoDownloadManager: DownloadManager): List<Int>? {
val cursor = exoDownloadManager.downloadIndex.getDownloads()
val ids: MutableList<Int> = mutableListOf()
while (cursor.moveToNext()) {
@ -61,7 +61,7 @@ class TracksRepository(override val context: Context?, albumId: Int) : @@ -61,7 +61,7 @@ class TracksRepository(override val context: Context?, albumId: Int) :
.toList()
.flatten()
val downloaded = getDownloadedIds() ?: listOf()
val downloaded = getDownloadedIds(exoDownloadManager) ?: listOf()
data.map { track ->
track.favorite = favorites.contains(track.id)
@ -70,7 +70,7 @@ class TracksRepository(override val context: Context?, albumId: Int) : @@ -70,7 +70,7 @@ class TracksRepository(override val context: Context?, albumId: Int) :
track.bestUpload()?.let { upload ->
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

2
app/src/main/java/audio/funkwhale/ffa/utils/AppContext.kt

@ -37,7 +37,7 @@ object AppContext { @@ -37,7 +37,7 @@ object AppContext {
cacheId = "$cacheId?$it"
}
Cache.set(context, cacheId, response.body().toByteArray())
FFACache.set(context, cacheId, response.body().toByteArray())
}
next(request, response)

79
app/src/main/java/audio/funkwhale/ffa/utils/Data.kt

@ -1,14 +1,6 @@ @@ -1,14 +1,6 @@
package audio.funkwhale.ffa.utils
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.File
import java.nio.charset.Charset
@ -16,76 +8,7 @@ import java.security.MessageDigest @@ -16,76 +8,7 @@ import java.security.MessageDigest
object RefreshError : Throwable()
class HTTP(val context: Context?) {
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 {
object FFACache {
private fun key(key: String): String {
val md = MessageDigest.getInstance("SHA-1")
val digest = md.digest(key.toByteArray(Charset.defaultCharset()))

11
app/src/main/java/audio/funkwhale/ffa/utils/Extensions.kt

@ -76,21 +76,20 @@ fun Picasso.maybeLoad(url: String?): RequestCreator { @@ -76,21 +76,20 @@ fun Picasso.maybeLoad(url: String?): RequestCreator {
else load(url)
}
fun Request.authorize(context: Context): Request {
fun Request.authorize(context: Context, oAuth: OAuth): Request {
return runBlocking {
this@authorize.apply {
if (!Settings.isAnonymous()) {
val oauth = OAuthFactory.instance()
oauth.state().let { state ->
oAuth.state().let { state ->
val old = state.accessToken
val auth = ClientSecretPost(oauth.state().clientSecret)
val auth = ClientSecretPost(oAuth.state().clientSecret)
val done = CompletableDeferred<Boolean>()
state.performActionWithFreshTokens(oauth.service(context), auth) { token, _, _ ->
state.performActionWithFreshTokens(oAuth.service(context), auth) { token, _, _ ->
if (token != old && token != null) {
state.save()
}
header("Authorization", "Bearer ${oauth.state().accessToken}")
header("Authorization", "Bearer ${oAuth.state().accessToken}")
done.complete(true)
}
done.await()

11
app/src/main/java/audio/funkwhale/ffa/utils/OAuth.kt

@ -51,17 +51,6 @@ interface OAuth { @@ -51,17 +51,6 @@ interface OAuth {
fun service(context: Context): AuthorizationService
}
object OAuthFactory {
private val oAuth: OAuth
init {
oAuth = DefaultOAuth(AuthorizationServiceFactory())
}
fun instance() = oAuth
}
class AuthorizationServiceFactory {
fun create(context: Context): AuthorizationService {

4
app/src/main/java/audio/funkwhale/ffa/utils/Userinfo.kt

@ -9,12 +9,12 @@ import com.preference.PowerPreference @@ -9,12 +9,12 @@ import com.preference.PowerPreference
object Userinfo {
suspend fun get(context: Context): User? {
suspend fun get(context: Context, oAuth: OAuth): User? {
try {
val hostname =
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("hostname")
val (_, _, result) = Fuel.get("$hostname/api/v1/users/users/me/")
.authorize(context)
.authorize(context, oAuth)
.awaitObjectResponseResult(gsonDeserializerOf(User::class.java))
return when (result) {

61
app/src/test/java/audio/funkwhale/ffa/FFATest.kt

@ -0,0 +1,61 @@ @@ -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()
}
}

27
app/src/test/java/audio/funkwhale/ffa/KoinTestApp.kt

@ -0,0 +1,27 @@ @@ -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)
}
}

85
app/src/test/java/audio/funkwhale/ffa/activities/SplashActivityTest.kt

@ -4,23 +4,98 @@ import android.content.Intent @@ -4,23 +4,98 @@ import android.content.Intent
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
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.runner.RunWith
import org.koin.core.context.stopKoin
import org.koin.dsl.module
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows
import org.robolectric.annotation.Config
import strikt.api.expectThat
import strikt.assertions.isEqualTo
@RunWith(RobolectricTestRunner::class)
@Config(application = KoinTestApp::class, sdk = [30])
class SplashActivityTest {
private val app: KoinTestApp = ApplicationProvider.getApplicationContext()
@After
fun tearDown() {
stopKoin()
}
@Test
fun `unauthorized and nonAnonymous request should redirect to LoginActivity`() {
val scenario = ActivityScenario.launch(SplashActivity::class.java)
scenario.onActivity { activity ->
val expectedIntent = Intent(activity, LoginActivity::class.java)
val appContext = Shadows.shadowOf(ApplicationProvider.getApplicationContext<FFA>())
expectThat(appContext.nextStartedActivity.component).isEqualTo(expectedIntent.component)
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)
scenario.onActivity { activity ->
val expectedIntent = Intent(activity, LoginActivity::class.java)
val appContext = Shadows.shadowOf(ApplicationProvider.getApplicationContext<FFA>())
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)
}
}
}
}

1
buildSrc/src/main/java/Versions.kt

@ -17,4 +17,5 @@ object Versions { @@ -17,4 +17,5 @@ object Versions {
const val strikt = "0.31.0"
const val androidXTest = "1.4.0"
const val robolectric = "4.6.1"
const val koin = "3.1.2"
}

Loading…
Cancel
Save