#7: Add Koin as dependency injection library

enhancement/speed-up-pipelines
Ryan Harg 2021-08-09 04:50:46 +00:00
rodzic 36771dcafb
commit a9319b88b0
37 zmienionych plików z 530 dodań i 437 usunięć

Wyświetl plik

@ -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
}

Wyświetl plik

@ -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() {
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() {
}
}
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() {
builder.appendLine(e.toString())
Cache.set(this@FFA, "crashdump", builder.toString().toByteArray())
FFACache.set(this@FFA, "crashdump", builder.toString().toByteArray())
}
}

Wyświetl plik

@ -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() {
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() {
}
private suspend fun refreshProgress() {
val cursor = FFA.get().exoDownloadManager.downloadIndex.getDownloads()
val cursor = exoDownloadManager.downloadIndex.getDownloads()
while (cursor.moveToNext()) {
val download = cursor.download

Wyświetl plik

@ -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
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() {
.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()

Wyświetl plik

@ -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() {
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() {
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() {
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() {
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() {
}
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() {
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() {
// 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() {
// 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() {
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()

Wyświetl plik

@ -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
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 :
"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 :
.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()
}

Wyświetl plik

@ -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)
}
}
}
}

Wyświetl plik

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

Wyświetl plik

@ -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() {
(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

Wyświetl plik

@ -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>() {
}
private suspend fun refreshDownloadedTracks() {
val downloaded = TracksRepository.getDownloadedIds() ?: listOf()
val downloaded = TracksRepository.getDownloadedIds(exoDownloadManager) ?: listOf()
withContext(Main) {
adapter.data = adapter.data.map {

Wyświetl plik

@ -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
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>() {
}
private suspend fun refreshDownloadedTracks() {
val downloaded = TracksRepository.getDownloadedIds() ?: listOf()
val downloaded = TracksRepository.getDownloadedIds(exoDownloadManager) ?: listOf()
withContext(Main) {
adapter.data = adapter.data.map {

Wyświetl plik

@ -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) }
}

Wyświetl plik

@ -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
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
}
}
FFA.get().mediaSession.connector.invalidateMediaSessionMetadata()
ffaMediaSession.connector.invalidateMediaSessionMetadata()
}
}
}

Wyświetl plik

@ -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
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) {
return super.onStartCommand(intent, flags, startId)
}
override fun getDownloadManager() = FFA.get().exoDownloadManager.apply {
override fun getDownloadManager() = exoDownloadManager.apply {
addListener(DownloadListener())
}

Wyświetl plik

@ -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.*
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() {
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() {
}
}
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() {
}
}
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() {
}
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() {
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() {
setPlaybackState(false)
player.release()
FFA.get().mediaSession.active = false
mediaSession.active = false
super.onDestroy()
}
@ -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() {
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() {
}
}
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() {
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()))

Wyświetl plik

@ -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
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 QueueManager(val context: Context) {
var metadata: MutableList<Track> = mutableListOf()
val datasources = ConcatenatingMediaSource()
var current = -1
class CacheDataSourceFactoryProvider(
private val oAuth: OAuth,
private val exoCache: Cache,
private val exoDownloadCache: Cache
) {
companion object {
fun create(context: Context): CacheDataSourceFactory {
fun factory(context: Context): CacheDataSourceFactory {
val playbackCache =
CacheDataSourceFactory(exoCache, createDatasourceFactory(context, oAuth))
val playbackCache =
CacheDataSourceFactory(FFA.get().exoCache, createDatasourceFactory(context))
return CacheDataSourceFactory(
FFA.get().exoDownloadCache,
playbackCache,
FileDataSource.Factory(),
null,
CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,
null
)
}
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
}
}
return CacheDataSourceFactory(
exoDownloadCache,
playbackCache,
FileDataSource.Factory(),
null,
CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,
null
)
}
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
}
}
}
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 {
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) {
}
}
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) {
}
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) {
}
metadata = tracks.toMutableList()
datasources.clear()
datasources.addMediaSources(sources)
dataSources.clear()
dataSources.addMediaSources(sources)
persist()
@ -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) {
}
metadata.addAll(tracks)
datasources.addMediaSources(sources)
dataSources.addMediaSources(sources)
persist()
@ -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) {
return
}
datasources.removeMediaSource(it)
dataSources.removeMediaSource(it)
metadata.removeAt(it)
if (it == current) {
@ -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) {
fun clear() {
metadata = mutableListOf()
datasources.clear()
dataSources.clear()
current = -1
persist()
@ -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)
}

Wyświetl plik

@ -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
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)
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) {
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) {
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) {
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) {
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) {
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) {
.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)

Wyświetl plik

@ -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"

Wyświetl plik

@ -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"

Wyświetl plik

@ -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"

Wyświetl plik

@ -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
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
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
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
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
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
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())
}
}
}

Wyświetl plik

@ -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>>(
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

Wyświetl plik

@ -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"

Wyświetl plik

@ -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
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,
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,
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?) :
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?) :
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?) :
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?) :
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}")
}
}

Wyświetl plik

@ -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"

Wyświetl plik

@ -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>> {
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>> {
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

Wyświetl plik

@ -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)
.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)
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)
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)
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>

Wyświetl plik

@ -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) :
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) :
.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) :
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

Wyświetl plik

@ -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)

Wyświetl plik

@ -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
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()))

Wyświetl plik

@ -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()

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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()
}
}

Wyświetl plik

@ -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)
}
}

Wyświetl plik

@ -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)
}
}
}
}

Wyświetl plik

@ -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"
}