Merge branch 'warnings-cleanup' into 'develop'
Cleanup most build warnings. See merge request funkwhale/funkwhale-android!211
This commit is contained in:
commit
10e67f1e80
|
@ -36,6 +36,8 @@ android {
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace = "audio.funkwhale.ffa"
|
||||||
|
|
||||||
testCoverage {
|
testCoverage {
|
||||||
version = "0.8.7"
|
version = "0.8.7"
|
||||||
}
|
}
|
||||||
|
@ -52,7 +54,7 @@ android {
|
||||||
disable += listOf("MissingTranslation", "ExtraTranslation")
|
disable += listOf("MissingTranslation", "ExtraTranslation")
|
||||||
}
|
}
|
||||||
|
|
||||||
compileSdk = 31
|
compileSdk = 33
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
|
|
||||||
|
@ -62,7 +64,7 @@ android {
|
||||||
versionName = androidGitVersion.name()
|
versionName = androidGitVersion.name()
|
||||||
|
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 30
|
targetSdk = 33
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
|
@ -167,18 +169,18 @@ dependencies {
|
||||||
implementation("com.google.android.material:material:1.6.1")
|
implementation("com.google.android.material:material:1.6.1")
|
||||||
implementation("com.android.support.constraint:constraint-layout:2.0.4")
|
implementation("com.android.support.constraint:constraint-layout:2.0.4")
|
||||||
|
|
||||||
implementation("com.google.android.exoplayer:exoplayer-core:2.14.2")
|
implementation("com.google.android.exoplayer:exoplayer-core:2.18.1")
|
||||||
implementation("com.google.android.exoplayer:exoplayer-ui:2.14.2")
|
implementation("com.google.android.exoplayer:exoplayer-ui:2.18.1")
|
||||||
implementation("com.google.android.exoplayer:extension-mediasession:2.14.2")
|
implementation("com.google.android.exoplayer:extension-mediasession:2.18.1")
|
||||||
|
|
||||||
implementation("io.insert-koin:koin-core:3.1.2")
|
implementation("io.insert-koin:koin-core:3.1.2")
|
||||||
implementation("io.insert-koin:koin-android:3.1.2")
|
implementation("io.insert-koin:koin-android:3.1.2")
|
||||||
testImplementation("io.insert-koin:koin-test:3.1.2")
|
testImplementation("io.insert-koin:koin-test:3.1.2")
|
||||||
|
|
||||||
implementation("com.github.PaulWoitaschek.ExoPlayer-Extensions:extension-opus:2.14.0") {
|
implementation("com.github.PaulWoitaschek.ExoPlayer-Extensions:extension-opus:789a4f83169cff5c7a91655bb828fde2cfde671a") {
|
||||||
isTransitive = false
|
isTransitive = false
|
||||||
}
|
}
|
||||||
implementation("com.github.PaulWoitaschek.ExoPlayer-Extensions:extension-flac:2.14.0") {
|
implementation("com.github.PaulWoitaschek.ExoPlayer-Extensions:extension-flac:789a4f83169cff5c7a91655bb828fde2cfde671a") {
|
||||||
isTransitive = false
|
isTransitive = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
package="audio.funkwhale.ffa">
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
@ -22,7 +21,8 @@
|
||||||
android:name=".activities.SplashActivity"
|
android:name=".activities.SplashActivity"
|
||||||
android:launchMode="singleInstance"
|
android:launchMode="singleInstance"
|
||||||
android:noHistory="true"
|
android:noHistory="true"
|
||||||
android:screenOrientation="portrait">
|
android:screenOrientation="portrait"
|
||||||
|
android:exported="true">
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
@ -61,7 +61,8 @@
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".playback.PlayerService"
|
android:name=".playback.PlayerService"
|
||||||
android:foregroundServiceType="mediaPlayback">
|
android:foregroundServiceType="mediaPlayback"
|
||||||
|
android:exported="false">
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||||
|
@ -80,7 +81,8 @@
|
||||||
|
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<receiver android:name="androidx.media.session.MediaButtonReceiver">
|
<receiver android:name="androidx.media.session.MediaButtonReceiver"
|
||||||
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
|
@ -6,13 +6,8 @@ import androidx.appcompat.app.AppCompatDelegate
|
||||||
import audio.funkwhale.ffa.koin.authModule
|
import audio.funkwhale.ffa.koin.authModule
|
||||||
import audio.funkwhale.ffa.koin.exoplayerModule
|
import audio.funkwhale.ffa.koin.exoplayerModule
|
||||||
import audio.funkwhale.ffa.utils.AppContext
|
import audio.funkwhale.ffa.utils.AppContext
|
||||||
import audio.funkwhale.ffa.utils.Command
|
|
||||||
import audio.funkwhale.ffa.utils.Event
|
|
||||||
import audio.funkwhale.ffa.utils.FFACache
|
import audio.funkwhale.ffa.utils.FFACache
|
||||||
import audio.funkwhale.ffa.utils.Request
|
|
||||||
import com.preference.PowerPreference
|
import com.preference.PowerPreference
|
||||||
import kotlinx.coroutines.channels.BroadcastChannel
|
|
||||||
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
|
|
||||||
import org.koin.core.context.startKoin
|
import org.koin.core.context.startKoin
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
@ -28,11 +23,6 @@ class FFA : Application() {
|
||||||
|
|
||||||
var defaultExceptionHandler: Thread.UncaughtExceptionHandler? = null
|
var defaultExceptionHandler: Thread.UncaughtExceptionHandler? = null
|
||||||
|
|
||||||
val eventBus: BroadcastChannel<Event> = BroadcastChannel(10)
|
|
||||||
val commandBus: BroadcastChannel<Command> = BroadcastChannel(10)
|
|
||||||
val requestBus: BroadcastChannel<Request> = BroadcastChannel(10)
|
|
||||||
val progressBus: BroadcastChannel<Triple<Int, Int, Int>> = ConflatedBroadcastChannel()
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import android.content.res.Configuration
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.view.doOnLayout
|
import androidx.core.view.doOnLayout
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
@ -40,30 +41,25 @@ class LoginActivity : AppCompatActivity() {
|
||||||
limitContainerWidth()
|
limitContainerWidth()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
private var resultLauncher =
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
registerForActivityResult(StartActivityForResult()) { result ->
|
||||||
|
result.data?.let {
|
||||||
|
oAuth.exchange(this, it) {
|
||||||
|
PowerPreference
|
||||||
|
.getFileByName(AppContext.PREFS_CREDENTIALS)
|
||||||
|
.setBoolean("anonymous", false)
|
||||||
|
|
||||||
data?.let {
|
lifecycleScope.launch(Main) {
|
||||||
when (requestCode) {
|
Userinfo.get(this@LoginActivity, oAuth)?.let {
|
||||||
0 -> {
|
startActivity(Intent(this@LoginActivity, MainActivity::class.java))
|
||||||
oAuth.exchange(this, data) {
|
|
||||||
PowerPreference
|
|
||||||
.getFileByName(AppContext.PREFS_CREDENTIALS)
|
|
||||||
.setBoolean("anonymous", false)
|
|
||||||
|
|
||||||
lifecycleScope.launch(Main) {
|
return@launch finish()
|
||||||
Userinfo.get(this@LoginActivity, oAuth)?.let {
|
|
||||||
startActivity(Intent(this@LoginActivity, MainActivity::class.java))
|
|
||||||
|
|
||||||
return@launch finish()
|
|
||||||
}
|
|
||||||
throw Exception(getString(R.string.login_error_userinfo))
|
|
||||||
}
|
}
|
||||||
|
throw Exception(getString(R.string.login_error_userinfo))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
@ -134,7 +130,7 @@ class LoginActivity : AppCompatActivity() {
|
||||||
oAuth.init(hostname)
|
oAuth.init(hostname)
|
||||||
return oAuth.register {
|
return oAuth.register {
|
||||||
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).setString("hostname", hostname)
|
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).setString("hostname", hostname)
|
||||||
oAuth.authorize(this)
|
resultLauncher.launch(oAuth.authorizeIntent(this))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.animation.AccelerateDecelerateInterpolator
|
import android.view.animation.AccelerateDecelerateInterpolator
|
||||||
import android.widget.SeekBar
|
import android.widget.SeekBar
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.widget.PopupMenu
|
import androidx.appcompat.widget.PopupMenu
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
@ -207,6 +208,21 @@ class MainActivity : AppCompatActivity() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var resultLauncher = registerForActivityResult(StartActivityForResult()) { result ->
|
||||||
|
if (result.resultCode == ResultCode.LOGOUT.code) {
|
||||||
|
Intent(this, LoginActivity::class.java).apply {
|
||||||
|
FFA.get().deleteAllData(this@MainActivity)
|
||||||
|
|
||||||
|
flags =
|
||||||
|
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||||
|
|
||||||
|
stopService(Intent(this@MainActivity, PlayerService::class.java))
|
||||||
|
startActivity(this)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
android.R.id.home -> {
|
android.R.id.home -> {
|
||||||
|
@ -228,8 +244,8 @@ class MainActivity : AppCompatActivity() {
|
||||||
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW)
|
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW)
|
||||||
item.actionView = View(this)
|
item.actionView = View(this)
|
||||||
item.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
|
item.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
|
||||||
override fun onMenuItemActionExpand(item: MenuItem?) = false
|
override fun onMenuItemActionExpand(item: MenuItem) = false
|
||||||
override fun onMenuItemActionCollapse(item: MenuItem?) = false
|
override fun onMenuItemActionCollapse(item: MenuItem) = false
|
||||||
})
|
})
|
||||||
|
|
||||||
item.isChecked = !item.isChecked
|
item.isChecked = !item.isChecked
|
||||||
|
@ -279,29 +295,12 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
R.id.nav_downloads -> startActivity(Intent(this, DownloadsActivity::class.java))
|
R.id.nav_downloads -> startActivity(Intent(this, DownloadsActivity::class.java))
|
||||||
R.id.settings -> startActivityForResult(Intent(this, SettingsActivity::class.java), 0)
|
R.id.settings -> resultLauncher.launch(Intent(this, SettingsActivity::class.java))
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
|
|
||||||
if (resultCode == ResultCode.LOGOUT.code) {
|
|
||||||
Intent(this, LoginActivity::class.java).apply {
|
|
||||||
FFA.get().deleteAllData(this@MainActivity)
|
|
||||||
|
|
||||||
flags =
|
|
||||||
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
|
||||||
|
|
||||||
stopService(Intent(this@MainActivity, PlayerService::class.java))
|
|
||||||
startActivity(this)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun launchFragment(fragment: Fragment) {
|
private fun launchFragment(fragment: Fragment) {
|
||||||
supportFragmentManager.fragments.lastOrNull()?.also { oldFragment ->
|
supportFragmentManager.fragments.lastOrNull()?.also { oldFragment ->
|
||||||
oldFragment.enterTransition = null
|
oldFragment.enterTransition = null
|
||||||
|
@ -359,7 +358,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
.alpha(0.0f)
|
.alpha(0.0f)
|
||||||
.setDuration(400)
|
.setDuration(400)
|
||||||
.setListener(object : AnimatorListenerAdapter() {
|
.setListener(object : AnimatorListenerAdapter() {
|
||||||
override fun onAnimationEnd(animator: Animator?) {
|
override fun onAnimationEnd(animator: Animator) {
|
||||||
binding.nowPlaying.visibility = View.GONE
|
binding.nowPlaying.visibility = View.GONE
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -474,11 +473,9 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
binding.nowPlayingContainer?.nowPlayingTitle?.text = track.title
|
binding.nowPlayingContainer?.nowPlayingTitle?.text = track.title
|
||||||
binding.nowPlayingContainer?.nowPlayingAlbum?.text = track.artist.name
|
binding.nowPlayingContainer?.nowPlayingAlbum?.text = track.artist.name
|
||||||
binding.nowPlayingContainer?.nowPlayingToggle?.icon = getDrawable(R.drawable.pause)
|
|
||||||
|
|
||||||
binding.nowPlayingContainer?.nowPlayingDetailsTitle?.text = track.title
|
binding.nowPlayingContainer?.nowPlayingDetailsTitle?.text = track.title
|
||||||
binding.nowPlayingContainer?.nowPlayingDetailsArtist?.text = track.artist.name
|
binding.nowPlayingContainer?.nowPlayingDetailsArtist?.text = track.artist.name
|
||||||
binding.nowPlayingContainer?.nowPlayingDetailsToggle?.icon = getDrawable(R.drawable.pause)
|
|
||||||
|
|
||||||
Picasso.get()
|
Picasso.get()
|
||||||
.maybeLoad(maybeNormalizeUrl(track.album?.cover?.urls?.original))
|
.maybeLoad(maybeNormalizeUrl(track.album?.cover?.urls?.original))
|
||||||
|
|
|
@ -59,7 +59,7 @@ class SettingsFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||||
when (preference?.key) {
|
when (preference.key) {
|
||||||
"oss_licences" -> startActivity(Intent(activity, LicencesActivity::class.java))
|
"oss_licences" -> startActivity(Intent(activity, LicencesActivity::class.java))
|
||||||
|
|
||||||
"crash" -> {
|
"crash" -> {
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
package audio.funkwhale.ffa.adapters
|
package audio.funkwhale.ffa.adapters
|
||||||
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
import androidx.fragment.app.FragmentPagerAdapter
|
|
||||||
import audio.funkwhale.ffa.R
|
import audio.funkwhale.ffa.R
|
||||||
import audio.funkwhale.ffa.fragments.AlbumsGridFragment
|
import audio.funkwhale.ffa.fragments.AlbumsGridFragment
|
||||||
import audio.funkwhale.ffa.fragments.ArtistsFragment
|
import audio.funkwhale.ffa.fragments.ArtistsFragment
|
||||||
|
@ -10,13 +9,13 @@ import audio.funkwhale.ffa.fragments.FavoritesFragment
|
||||||
import audio.funkwhale.ffa.fragments.PlaylistsFragment
|
import audio.funkwhale.ffa.fragments.PlaylistsFragment
|
||||||
import audio.funkwhale.ffa.fragments.RadiosFragment
|
import audio.funkwhale.ffa.fragments.RadiosFragment
|
||||||
|
|
||||||
class BrowseTabsAdapter(val context: Fragment, manager: FragmentManager) :
|
class BrowseTabsAdapter(val context: Fragment) :
|
||||||
FragmentPagerAdapter(manager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
|
FragmentStateAdapter(context) {
|
||||||
var tabs = mutableListOf<Fragment>()
|
var tabs = mutableListOf<Fragment>()
|
||||||
|
|
||||||
override fun getCount() = 5
|
override fun getItemCount() = 5
|
||||||
|
|
||||||
override fun getItem(position: Int): Fragment {
|
override fun createFragment(position: Int): Fragment {
|
||||||
tabs.getOrNull(position)?.let {
|
tabs.getOrNull(position)?.let {
|
||||||
return it
|
return it
|
||||||
}
|
}
|
||||||
|
@ -35,7 +34,7 @@ class BrowseTabsAdapter(val context: Fragment, manager: FragmentManager) :
|
||||||
return fragment
|
return fragment
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPageTitle(position: Int): String {
|
fun tabText(position: Int): String {
|
||||||
return when (position) {
|
return when (position) {
|
||||||
0 -> context.getString(R.string.artists)
|
0 -> context.getString(R.string.artists)
|
||||||
1 -> context.getString(R.string.albums)
|
1 -> context.getString(R.string.albums)
|
||||||
|
|
|
@ -7,6 +7,7 @@ import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import audio.funkwhale.ffa.adapters.BrowseTabsAdapter
|
import audio.funkwhale.ffa.adapters.BrowseTabsAdapter
|
||||||
import audio.funkwhale.ffa.databinding.FragmentBrowseBinding
|
import audio.funkwhale.ffa.databinding.FragmentBrowseBinding
|
||||||
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
|
|
||||||
class BrowseFragment : Fragment() {
|
class BrowseFragment : Fragment() {
|
||||||
|
|
||||||
|
@ -17,7 +18,7 @@ class BrowseFragment : Fragment() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
adapter = BrowseTabsAdapter(this, childFragmentManager)
|
adapter = BrowseTabsAdapter(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
|
@ -27,11 +28,13 @@ class BrowseFragment : Fragment() {
|
||||||
): View {
|
): View {
|
||||||
_binding = FragmentBrowseBinding.inflate(inflater)
|
_binding = FragmentBrowseBinding.inflate(inflater)
|
||||||
return binding.root.apply {
|
return binding.root.apply {
|
||||||
binding.tabs.setupWithViewPager(binding.pager)
|
|
||||||
binding.tabs.getTabAt(0)?.select()
|
binding.tabs.getTabAt(0)?.select()
|
||||||
|
|
||||||
binding.pager.adapter = adapter
|
binding.pager.adapter = adapter
|
||||||
binding.pager.offscreenPageLimit = 3
|
binding.pager.offscreenPageLimit = 3
|
||||||
|
TabLayoutMediator(binding.tabs, binding.pager) { tab, position ->
|
||||||
|
tab.text = adapter?.tabText(position)
|
||||||
|
}.attach()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import audio.funkwhale.ffa.playback.MediaSession
|
||||||
import audio.funkwhale.ffa.utils.AuthorizationServiceFactory
|
import audio.funkwhale.ffa.utils.AuthorizationServiceFactory
|
||||||
import audio.funkwhale.ffa.utils.OAuth
|
import audio.funkwhale.ffa.utils.OAuth
|
||||||
import com.google.android.exoplayer2.database.DatabaseProvider
|
import com.google.android.exoplayer2.database.DatabaseProvider
|
||||||
import com.google.android.exoplayer2.database.ExoDatabaseProvider
|
import com.google.android.exoplayer2.database.StandaloneDatabaseProvider
|
||||||
import com.google.android.exoplayer2.offline.DownloadManager
|
import com.google.android.exoplayer2.offline.DownloadManager
|
||||||
import com.google.android.exoplayer2.upstream.cache.Cache
|
import com.google.android.exoplayer2.upstream.cache.Cache
|
||||||
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor
|
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor
|
||||||
|
@ -19,7 +19,7 @@ import org.koin.dsl.module
|
||||||
fun exoplayerModule(context: Context) = module {
|
fun exoplayerModule(context: Context) = module {
|
||||||
|
|
||||||
single<DatabaseProvider>(named("exoDatabase")) {
|
single<DatabaseProvider>(named("exoDatabase")) {
|
||||||
ExoDatabaseProvider(context)
|
StandaloneDatabaseProvider(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
single {
|
single {
|
||||||
|
|
|
@ -2,6 +2,7 @@ package audio.funkwhale.ffa.playback
|
||||||
|
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
|
import android.app.PendingIntent.FLAG_IMMUTABLE
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.support.v4.media.session.MediaSessionCompat
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
|
@ -21,7 +22,11 @@ import kotlinx.coroutines.Dispatchers.Default
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.java.KoinJavaComponent.inject
|
import org.koin.java.KoinJavaComponent.inject
|
||||||
|
|
||||||
class MediaControlsManager(val context: Service, private val scope: CoroutineScope, private val mediaSession: MediaSessionCompat) {
|
class MediaControlsManager(
|
||||||
|
val context: Service,
|
||||||
|
private val scope: CoroutineScope,
|
||||||
|
private val mediaSession: MediaSessionCompat
|
||||||
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val NOTIFICATION_ACTION_OPEN_QUEUE = 0
|
const val NOTIFICATION_ACTION_OPEN_QUEUE = 0
|
||||||
|
@ -41,8 +46,10 @@ class MediaControlsManager(val context: Service, private val scope: CoroutineSco
|
||||||
}
|
}
|
||||||
|
|
||||||
scope.launch(Default) {
|
scope.launch(Default) {
|
||||||
val openIntent = Intent(context, MainActivity::class.java).apply { action = NOTIFICATION_ACTION_OPEN_QUEUE.toString() }
|
val openIntent = Intent(context, MainActivity::class.java).apply {
|
||||||
val openPendingIntent = PendingIntent.getActivity(context, 0, openIntent, 0)
|
action = NOTIFICATION_ACTION_OPEN_QUEUE.toString()
|
||||||
|
}
|
||||||
|
val openPendingIntent = PendingIntent.getActivity(context, 0, openIntent, FLAG_IMMUTABLE)
|
||||||
|
|
||||||
val coverUrl = maybeNormalizeUrl(track.album?.cover())
|
val coverUrl = maybeNormalizeUrl(track.album?.cover())
|
||||||
|
|
||||||
|
@ -98,7 +105,8 @@ class MediaControlsManager(val context: Service, private val scope: CoroutineSco
|
||||||
if (playing) {
|
if (playing) {
|
||||||
context.startForeground(AppContext.NOTIFICATION_MEDIA_CONTROL, it)
|
context.startForeground(AppContext.NOTIFICATION_MEDIA_CONTROL, it)
|
||||||
} else {
|
} else {
|
||||||
NotificationManagerCompat.from(context).notify(AppContext.NOTIFICATION_MEDIA_CONTROL, it)
|
NotificationManagerCompat.from(context)
|
||||||
|
.notify(AppContext.NOTIFICATION_MEDIA_CONTROL, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,6 @@ import android.support.v4.media.session.MediaSessionCompat
|
||||||
import android.support.v4.media.session.PlaybackStateCompat
|
import android.support.v4.media.session.PlaybackStateCompat
|
||||||
import audio.funkwhale.ffa.utils.Command
|
import audio.funkwhale.ffa.utils.Command
|
||||||
import audio.funkwhale.ffa.utils.CommandBus
|
import audio.funkwhale.ffa.utils.CommandBus
|
||||||
import com.google.android.exoplayer2.ControlDispatcher
|
|
||||||
import com.google.android.exoplayer2.Player
|
import com.google.android.exoplayer2.Player
|
||||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
||||||
|
|
||||||
|
@ -31,7 +30,6 @@ class MediaSession(private val context: Context) {
|
||||||
|
|
||||||
val session: MediaSessionCompat by lazy {
|
val session: MediaSessionCompat by lazy {
|
||||||
MediaSessionCompat(context, context.packageName).apply {
|
MediaSessionCompat(context, context.packageName).apply {
|
||||||
setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS)
|
|
||||||
setPlaybackState(playbackStateBuilder.build())
|
setPlaybackState(playbackStateBuilder.build())
|
||||||
|
|
||||||
isActive = true
|
isActive = true
|
||||||
|
@ -43,7 +41,7 @@ class MediaSession(private val context: Context) {
|
||||||
MediaSessionConnector(session).also {
|
MediaSessionConnector(session).also {
|
||||||
it.setQueueNavigator(FFAQueueNavigator())
|
it.setQueueNavigator(FFAQueueNavigator())
|
||||||
|
|
||||||
it.setMediaButtonEventHandler { _, _, intent ->
|
it.setMediaButtonEventHandler { _, intent ->
|
||||||
if (!active) {
|
if (!active) {
|
||||||
Intent(context, PlayerService::class.java).let { player ->
|
Intent(context, PlayerService::class.java).let { player ->
|
||||||
player.action = intent.action
|
player.action = intent.action
|
||||||
|
@ -67,13 +65,11 @@ class MediaSession(private val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
class FFAQueueNavigator : MediaSessionConnector.QueueNavigator {
|
class FFAQueueNavigator : MediaSessionConnector.QueueNavigator {
|
||||||
override fun onSkipToQueueItem(player: Player, controlDispatcher: ControlDispatcher, id: Long) {
|
override fun onSkipToQueueItem(player: Player, id: Long) {
|
||||||
CommandBus.send(Command.PlayTrack(id.toInt()))
|
CommandBus.send(Command.PlayTrack(id.toInt()))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCurrentWindowIndexChanged(player: Player) {}
|
override fun onCommand(player: Player, command: String, extras: Bundle?, cb: ResultReceiver?) = true
|
||||||
|
|
||||||
override fun onCommand(player: Player, controlDispatcher: ControlDispatcher, command: String, extras: Bundle?, cb: ResultReceiver?) = true
|
|
||||||
|
|
||||||
override fun getSupportedQueueNavigatorActions(player: Player): Long {
|
override fun getSupportedQueueNavigatorActions(player: Player): Long {
|
||||||
return PlaybackStateCompat.ACTION_PLAY_PAUSE or
|
return PlaybackStateCompat.ACTION_PLAY_PAUSE or
|
||||||
|
@ -82,13 +78,13 @@ class FFAQueueNavigator : MediaSessionConnector.QueueNavigator {
|
||||||
PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM
|
PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSkipToNext(player: Player, controlDispatcher: ControlDispatcher) {
|
override fun onSkipToNext(player: Player) {
|
||||||
CommandBus.send(Command.NextTrack)
|
CommandBus.send(Command.NextTrack)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getActiveQueueItemId(player: Player?) = player?.currentWindowIndex?.toLong() ?: 0
|
override fun getActiveQueueItemId(player: Player?) = player?.currentMediaItemIndex?.toLong() ?: 0
|
||||||
|
|
||||||
override fun onSkipToPrevious(player: Player, controlDispatcher: ControlDispatcher) {
|
override fun onSkipToPrevious(player: Player) {
|
||||||
CommandBus.send(Command.PreviousTrack)
|
CommandBus.send(Command.PreviousTrack)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -80,14 +80,20 @@ class PinService : DownloadService(AppContext.NOTIFICATION_DOWNLOADS) {
|
||||||
|
|
||||||
override fun getScheduler(): Scheduler? = null
|
override fun getScheduler(): Scheduler? = null
|
||||||
|
|
||||||
override fun getForegroundNotification(downloads: MutableList<Download>): Notification {
|
override fun getForegroundNotification(
|
||||||
|
downloads: MutableList<Download>,
|
||||||
|
notMetRequirements: Int
|
||||||
|
): Notification {
|
||||||
val description =
|
val description =
|
||||||
resources.getQuantityString(R.plurals.downloads_description, downloads.size, downloads.size)
|
resources.getQuantityString(R.plurals.downloads_description, downloads.size, downloads.size)
|
||||||
|
|
||||||
return DownloadNotificationHelper(
|
return DownloadNotificationHelper(
|
||||||
this,
|
this,
|
||||||
AppContext.NOTIFICATION_CHANNEL_DOWNLOADS
|
AppContext.NOTIFICATION_CHANNEL_DOWNLOADS
|
||||||
).buildProgressNotification(this, R.drawable.downloads, null, description, downloads)
|
).buildProgressNotification(
|
||||||
|
this, R.drawable.downloads, null, description,
|
||||||
|
downloads, notMetRequirements
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDownloads() = downloadManager.downloadIndex.getDownloads()
|
private fun getDownloads() = downloadManager.downloadIndex.getDownloads()
|
||||||
|
|
|
@ -31,11 +31,10 @@ import audio.funkwhale.ffa.utils.log
|
||||||
import audio.funkwhale.ffa.utils.maybeNormalizeUrl
|
import audio.funkwhale.ffa.utils.maybeNormalizeUrl
|
||||||
import audio.funkwhale.ffa.utils.onApi
|
import audio.funkwhale.ffa.utils.onApi
|
||||||
import com.google.android.exoplayer2.C
|
import com.google.android.exoplayer2.C
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException
|
import com.google.android.exoplayer2.ExoPlayer
|
||||||
|
import com.google.android.exoplayer2.PlaybackException
|
||||||
import com.google.android.exoplayer2.Player
|
import com.google.android.exoplayer2.Player
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer
|
import com.google.android.exoplayer2.Tracks
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray
|
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray
|
|
||||||
import com.squareup.picasso.Picasso
|
import com.squareup.picasso.Picasso
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
|
@ -65,7 +64,7 @@ class PlayerService : Service() {
|
||||||
|
|
||||||
private lateinit var queue: QueueManager
|
private lateinit var queue: QueueManager
|
||||||
private lateinit var mediaControlsManager: MediaControlsManager
|
private lateinit var mediaControlsManager: MediaControlsManager
|
||||||
private lateinit var player: SimpleExoPlayer
|
private lateinit var player: ExoPlayer
|
||||||
|
|
||||||
private val mediaMetadataBuilder = MediaMetadataCompat.Builder()
|
private val mediaMetadataBuilder = MediaMetadataCompat.Builder()
|
||||||
|
|
||||||
|
@ -132,12 +131,13 @@ class PlayerService : Service() {
|
||||||
|
|
||||||
mediaControlsManager = MediaControlsManager(this, scope, mediaSession.session)
|
mediaControlsManager = MediaControlsManager(this, scope, mediaSession.session)
|
||||||
|
|
||||||
player = SimpleExoPlayer.Builder(this).build().apply {
|
player = ExoPlayer.Builder(this).build().apply {
|
||||||
playWhenReady = false
|
playWhenReady = false
|
||||||
|
|
||||||
playerEventListener = PlayerEventListener().also {
|
playerEventListener = PlayerEventListener().also {
|
||||||
addListener(it)
|
addListener(it)
|
||||||
}
|
}
|
||||||
|
EventBus.send(Event.StateChanged(this.isPlaying()))
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaSession.active = true
|
mediaSession.active = true
|
||||||
|
@ -151,7 +151,8 @@ class PlayerService : Service() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (queue.current > -1) {
|
if (queue.current > -1) {
|
||||||
player.prepare(queue.dataSources)
|
player.setMediaSource(queue.dataSources)
|
||||||
|
player.prepare()
|
||||||
|
|
||||||
FFACache.getLine(this, "progress")?.let {
|
FFACache.getLine(this, "progress")?.let {
|
||||||
player.seekTo(queue.current, it.toLong())
|
player.seekTo(queue.current, it.toLong())
|
||||||
|
@ -180,7 +181,8 @@ class PlayerService : Service() {
|
||||||
if (!command.fromRadio) radioPlayer.stop()
|
if (!command.fromRadio) radioPlayer.stop()
|
||||||
|
|
||||||
queue.replace(command.queue)
|
queue.replace(command.queue)
|
||||||
player.prepare(queue.dataSources, true, true)
|
player.setMediaSource(queue.dataSources)
|
||||||
|
player.prepare()
|
||||||
|
|
||||||
setPlaybackState(true)
|
setPlaybackState(true)
|
||||||
|
|
||||||
|
@ -307,7 +309,8 @@ class PlayerService : Service() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state && player.playbackState == Player.STATE_IDLE) {
|
if (state && player.playbackState == Player.STATE_IDLE) {
|
||||||
player.prepare(queue.dataSources)
|
player.setMediaSource(queue.dataSources)
|
||||||
|
player.prepare()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasAudioFocus(state)) {
|
if (hasAudioFocus(state)) {
|
||||||
|
@ -318,7 +321,7 @@ class PlayerService : Service() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun togglePlayback() {
|
private fun togglePlayback() {
|
||||||
setPlaybackState(!player.playWhenReady)
|
setPlaybackState(!player.isPlaying)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun skipToPreviousTrack() {
|
private fun skipToPreviousTrack() {
|
||||||
|
@ -326,11 +329,11 @@ class PlayerService : Service() {
|
||||||
return player.seekTo(0)
|
return player.seekTo(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
player.previous()
|
player.seekToPrevious()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun skipToNextTrack() {
|
private fun skipToNextTrack() {
|
||||||
player.next()
|
player.seekToNext()
|
||||||
|
|
||||||
FFACache.set(this@PlayerService, "progress", "0")
|
FFACache.set(this@PlayerService, "progress", "0")
|
||||||
ProgressBus.send(0, 0, 0)
|
ProgressBus.send(0, 0, 0)
|
||||||
|
@ -419,9 +422,14 @@ class PlayerService : Service() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
@SuppressLint("NewApi")
|
||||||
inner class PlayerEventListener : Player.EventListener {
|
inner class PlayerEventListener : Player.Listener {
|
||||||
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
|
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||||
super.onPlayerStateChanged(playWhenReady, playbackState)
|
super.onIsPlayingChanged(isPlaying)
|
||||||
|
mediaControlsManager.updateNotification(queue.current(), isPlaying)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
|
||||||
|
super.onPlayWhenReadyChanged(playWhenReady, reason)
|
||||||
|
|
||||||
EventBus.send(Event.StateChanged(playWhenReady))
|
EventBus.send(Event.StateChanged(playWhenReady))
|
||||||
|
|
||||||
|
@ -429,55 +437,45 @@ class PlayerService : Service() {
|
||||||
CommandBus.send(Command.RefreshTrack(queue.current()))
|
CommandBus.send(Command.RefreshTrack(queue.current()))
|
||||||
}
|
}
|
||||||
|
|
||||||
when (playWhenReady) {
|
if (!playWhenReady) {
|
||||||
true -> {
|
Build.VERSION_CODES.N.onApi(
|
||||||
when (playbackState) {
|
{ stopForeground(STOP_FOREGROUND_DETACH) },
|
||||||
Player.STATE_READY -> mediaControlsManager.updateNotification(queue.current(), true)
|
{ stopForeground(false) }
|
||||||
Player.STATE_BUFFERING -> EventBus.send(Event.Buffering(true))
|
)
|
||||||
Player.STATE_ENDED -> {
|
}
|
||||||
setPlaybackState(false)
|
}
|
||||||
|
|
||||||
queue.current = 0
|
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||||
player.seekTo(0, C.TIME_UNSET)
|
super.onPlaybackStateChanged(playbackState)
|
||||||
|
EventBus.send(Event.Buffering(playbackState == Player.STATE_BUFFERING))
|
||||||
|
when (playbackState) {
|
||||||
|
Player.STATE_ENDED -> {
|
||||||
|
setPlaybackState(false)
|
||||||
|
|
||||||
ProgressBus.send(0, 0, 0)
|
queue.current = 0
|
||||||
}
|
player.seekTo(0, C.TIME_UNSET)
|
||||||
|
|
||||||
Player.STATE_IDLE -> {
|
ProgressBus.send(0, 0, 0)
|
||||||
setPlaybackState(false)
|
|
||||||
|
|
||||||
return EventBus.send(Event.PlaybackStopped)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (playbackState != Player.STATE_BUFFERING) EventBus.send(Event.Buffering(false))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
false -> {
|
Player.STATE_IDLE -> {
|
||||||
EventBus.send(Event.Buffering(false))
|
setPlaybackState(false)
|
||||||
|
|
||||||
Build.VERSION_CODES.N.onApi(
|
EventBus.send(Event.PlaybackStopped)
|
||||||
{ stopForeground(STOP_FOREGROUND_DETACH) },
|
|
||||||
{ stopForeground(false) }
|
|
||||||
)
|
|
||||||
|
|
||||||
when (playbackState) {
|
if (!player.playWhenReady) {
|
||||||
Player.STATE_READY -> mediaControlsManager.updateNotification(queue.current(), false)
|
mediaControlsManager.remove()
|
||||||
Player.STATE_IDLE -> mediaControlsManager.remove()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTracksChanged(
|
override fun onTracksChanged(tracks: Tracks) {
|
||||||
trackGroups: TrackGroupArray,
|
super.onTracksChanged(tracks)
|
||||||
trackSelections: TrackSelectionArray
|
|
||||||
) {
|
|
||||||
super.onTracksChanged(trackGroups, trackSelections)
|
|
||||||
|
|
||||||
if (queue.current != player.currentWindowIndex) {
|
if (queue.current != player.currentMediaItemIndex) {
|
||||||
queue.current = player.currentWindowIndex
|
queue.current = player.currentMediaItemIndex
|
||||||
mediaControlsManager.updateNotification(queue.current(), player.playWhenReady)
|
mediaControlsManager.updateNotification(queue.current(), player.isPlaying)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (queue.get().isNotEmpty() &&
|
if (queue.get().isNotEmpty() &&
|
||||||
|
@ -510,13 +508,14 @@ class PlayerService : Service() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlayerError(error: ExoPlaybackException) {
|
override fun onPlayerError(error: PlaybackException) {
|
||||||
EventBus.send(Event.PlaybackError(getString(R.string.error_playback)))
|
EventBus.send(Event.PlaybackError(getString(R.string.error_playback)))
|
||||||
|
|
||||||
if (player.playWhenReady) {
|
if (player.playWhenReady) {
|
||||||
queue.current++
|
queue.current++
|
||||||
player.prepare(queue.dataSources, true, true)
|
player.setMediaSource(queue.dataSources, true)
|
||||||
player.seekTo(queue.current, 0)
|
player.seekTo(queue.current, 0)
|
||||||
|
player.prepare()
|
||||||
|
|
||||||
CommandBus.send(Command.RefreshTrack(queue.current()))
|
CommandBus.send(Command.RefreshTrack(queue.current()))
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import audio.funkwhale.ffa.utils.FFACache
|
||||||
import audio.funkwhale.ffa.utils.log
|
import audio.funkwhale.ffa.utils.log
|
||||||
import audio.funkwhale.ffa.utils.mustNormalizeUrl
|
import audio.funkwhale.ffa.utils.mustNormalizeUrl
|
||||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||||
|
import com.google.android.exoplayer2.MediaItem
|
||||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource
|
import com.google.android.exoplayer2.source.ConcatenatingMediaSource
|
||||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
|
@ -38,8 +39,8 @@ class QueueManager(val context: Context) {
|
||||||
metadata.map { track ->
|
metadata.map { track ->
|
||||||
val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "")
|
val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "")
|
||||||
|
|
||||||
ProgressiveMediaSource.Factory(factory).setTag(track.title)
|
val mediaItem = MediaItem.fromUri(Uri.parse(url)).buildUpon().setTag(track.title).build()
|
||||||
.createMediaSource(Uri.parse(url))
|
ProgressiveMediaSource.Factory(factory).createMediaSource(mediaItem)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -63,8 +64,8 @@ class QueueManager(val context: Context) {
|
||||||
val factory = cacheDataSourceFactoryProvider.create(context)
|
val factory = cacheDataSourceFactoryProvider.create(context)
|
||||||
val sources = tracks.map { track ->
|
val sources = tracks.map { track ->
|
||||||
val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "")
|
val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "")
|
||||||
|
val mediaItem = MediaItem.fromUri(Uri.parse(url)).buildUpon().setTag(track.title).build()
|
||||||
ProgressiveMediaSource.Factory(factory).setTag(track.title).createMediaSource(Uri.parse(url))
|
ProgressiveMediaSource.Factory(factory).createMediaSource(mediaItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
metadata = tracks.toMutableList()
|
metadata = tracks.toMutableList()
|
||||||
|
@ -84,7 +85,8 @@ class QueueManager(val context: Context) {
|
||||||
val sources = missingTracks.map { track ->
|
val sources = missingTracks.map { track ->
|
||||||
val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "")
|
val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "")
|
||||||
|
|
||||||
ProgressiveMediaSource.Factory(factory).createMediaSource(Uri.parse(url))
|
val mediaItem = MediaItem.fromUri(Uri.parse(url)).buildUpon().setTag(track.title).build()
|
||||||
|
ProgressiveMediaSource.Factory(factory).createMediaSource(mediaItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
metadata.addAll(tracks)
|
metadata.addAll(tracks)
|
||||||
|
@ -101,7 +103,8 @@ class QueueManager(val context: Context) {
|
||||||
val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "")
|
val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "")
|
||||||
|
|
||||||
if (metadata.indexOf(track) == -1) {
|
if (metadata.indexOf(track) == -1) {
|
||||||
ProgressiveMediaSource.Factory(factory).createMediaSource(Uri.parse(url)).let {
|
val mediaItem = MediaItem.fromUri(Uri.parse(url)).buildUpon().setTag(track.title).build()
|
||||||
|
ProgressiveMediaSource.Factory(factory).createMediaSource(mediaItem).let {
|
||||||
dataSources.addMediaSource(current + 1, it)
|
dataSources.addMediaSource(current + 1, it)
|
||||||
metadata.add(current + 1, track)
|
metadata.add(current + 1, track)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,8 +11,8 @@ import audio.funkwhale.ffa.model.Track
|
||||||
import audio.funkwhale.ffa.model.TracksCache
|
import audio.funkwhale.ffa.model.TracksCache
|
||||||
import audio.funkwhale.ffa.model.TracksResponse
|
import audio.funkwhale.ffa.model.TracksResponse
|
||||||
import audio.funkwhale.ffa.utils.OAuth
|
import audio.funkwhale.ffa.utils.OAuth
|
||||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
|
||||||
import audio.funkwhale.ffa.utils.mustNormalizeUrl
|
import audio.funkwhale.ffa.utils.mustNormalizeUrl
|
||||||
|
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||||
import com.google.android.exoplayer2.offline.DownloadManager
|
import com.google.android.exoplayer2.offline.DownloadManager
|
||||||
import com.google.android.exoplayer2.upstream.cache.Cache
|
import com.google.android.exoplayer2.upstream.cache.Cache
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
|
|
|
@ -7,8 +7,8 @@ import audio.funkwhale.ffa.model.TracksCache
|
||||||
import audio.funkwhale.ffa.model.TracksResponse
|
import audio.funkwhale.ffa.model.TracksResponse
|
||||||
import audio.funkwhale.ffa.utils.OAuth
|
import audio.funkwhale.ffa.utils.OAuth
|
||||||
import audio.funkwhale.ffa.utils.getMetadata
|
import audio.funkwhale.ffa.utils.getMetadata
|
||||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
|
||||||
import audio.funkwhale.ffa.utils.mustNormalizeUrl
|
import audio.funkwhale.ffa.utils.mustNormalizeUrl
|
||||||
|
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||||
import com.google.android.exoplayer2.offline.Download
|
import com.google.android.exoplayer2.offline.Download
|
||||||
import com.google.android.exoplayer2.offline.DownloadManager
|
import com.google.android.exoplayer2.offline.DownloadManager
|
||||||
import com.google.android.exoplayer2.upstream.cache.Cache
|
import com.google.android.exoplayer2.upstream.cache.Cache
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package audio.funkwhale.ffa.utils
|
package audio.funkwhale.ffa.utils
|
||||||
|
|
||||||
import audio.funkwhale.ffa.FFA
|
|
||||||
import audio.funkwhale.ffa.model.Radio
|
import audio.funkwhale.ffa.model.Radio
|
||||||
import audio.funkwhale.ffa.model.Track
|
import audio.funkwhale.ffa.model.Track
|
||||||
import com.google.android.exoplayer2.offline.Download
|
import com.google.android.exoplayer2.offline.Download
|
||||||
|
@ -8,8 +7,10 @@ import com.google.android.exoplayer2.offline.DownloadCursor
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.asFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.conflate
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
sealed class Command {
|
sealed class Command {
|
||||||
|
@ -71,47 +72,53 @@ sealed class Response {
|
||||||
}
|
}
|
||||||
|
|
||||||
object EventBus {
|
object EventBus {
|
||||||
|
private var _events = MutableSharedFlow<Event>()
|
||||||
|
val events = _events.asSharedFlow()
|
||||||
fun send(event: Event) {
|
fun send(event: Event) {
|
||||||
GlobalScope.launch(IO) {
|
GlobalScope.launch(IO) {
|
||||||
FFA.get().eventBus.trySend(event).isSuccess
|
_events.emit(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun get() = FFA.get().eventBus.asFlow()
|
fun get() = events
|
||||||
}
|
}
|
||||||
|
|
||||||
object CommandBus {
|
object CommandBus {
|
||||||
|
private var _commands = MutableSharedFlow<Command>()
|
||||||
|
var commands = _commands.asSharedFlow()
|
||||||
fun send(command: Command) {
|
fun send(command: Command) {
|
||||||
GlobalScope.launch(IO) {
|
GlobalScope.launch(IO) {
|
||||||
FFA.get().commandBus.trySend(command).isSuccess
|
_commands.emit(command)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun get() = FFA.get().commandBus.asFlow()
|
fun get() = commands
|
||||||
}
|
}
|
||||||
|
|
||||||
object RequestBus {
|
object RequestBus {
|
||||||
|
private var _requests = MutableSharedFlow<Request>()
|
||||||
|
var requests = _requests.asSharedFlow()
|
||||||
fun send(request: Request): Channel<Response> {
|
fun send(request: Request): Channel<Response> {
|
||||||
return Channel<Response>().also {
|
return Channel<Response>().also {
|
||||||
GlobalScope.launch(IO) {
|
GlobalScope.launch(IO) {
|
||||||
request.channel = it
|
request.channel = it
|
||||||
|
|
||||||
FFA.get().requestBus.trySend(request).isSuccess
|
_requests.emit(request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun get() = FFA.get().requestBus.asFlow()
|
fun get() = requests
|
||||||
}
|
}
|
||||||
|
|
||||||
object ProgressBus {
|
object ProgressBus {
|
||||||
|
private var _progress = MutableStateFlow(Triple(0, 0, 0))
|
||||||
|
val progress = _progress.asStateFlow()
|
||||||
fun send(current: Int, duration: Int, percent: Int) {
|
fun send(current: Int, duration: Int, percent: Int) {
|
||||||
GlobalScope.launch(IO) {
|
_progress.value = Triple(current, duration, percent)
|
||||||
FFA.get().progressBus.send(Triple(current, duration, percent))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun get() = FFA.get().progressBus.asFlow().conflate()
|
fun get() = progress
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend inline fun <reified T> Channel<Response>.wait(): T? {
|
suspend inline fun <reified T> Channel<Response>.wait(): T? {
|
||||||
|
|
|
@ -10,10 +10,8 @@ import audio.funkwhale.ffa.model.DownloadInfo
|
||||||
import audio.funkwhale.ffa.repositories.Repository
|
import audio.funkwhale.ffa.repositories.Repository
|
||||||
import com.github.kittinunf.fuel.core.FuelError
|
import com.github.kittinunf.fuel.core.FuelError
|
||||||
import com.github.kittinunf.fuel.core.Request
|
import com.github.kittinunf.fuel.core.Request
|
||||||
import com.github.kittinunf.fuel.core.ResponseDeserializable
|
|
||||||
import com.google.android.exoplayer2.offline.Download
|
import com.google.android.exoplayer2.offline.Download
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.reflect.TypeToken
|
|
||||||
import com.squareup.picasso.Picasso
|
import com.squareup.picasso.Picasso
|
||||||
import com.squareup.picasso.RequestCreator
|
import com.squareup.picasso.RequestCreator
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
@ -23,7 +21,6 @@ import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import net.openid.appauth.ClientSecretPost
|
import net.openid.appauth.ClientSecretPost
|
||||||
import java.io.Reader
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
|
@ -184,11 +184,10 @@ class OAuth(private val authorizationServiceFactory: AuthorizationServiceFactory
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun authorize(activity: Activity) {
|
fun authorizeIntent(activity: Activity): Intent? {
|
||||||
val authService = service(activity)
|
val authService = service(activity)
|
||||||
authorizationRequest()?.let { it ->
|
return authorizationRequest()?.let { it ->
|
||||||
val intent = authService.getAuthorizationRequestIntent(it)
|
authService.getAuthorizationRequestIntent(it)
|
||||||
activity.startActivityForResult(intent, 0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,7 @@ class NowPlayingView : MaterialCardView {
|
||||||
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
|
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
|
||||||
override fun onGlobalLayout() {
|
override fun onGlobalLayout() {
|
||||||
gestureDetectorCallback = OnGestureDetection()
|
gestureDetectorCallback = OnGestureDetection()
|
||||||
gestureDetector = GestureDetector(context, gestureDetectorCallback)
|
gestureDetector = GestureDetector(context, gestureDetectorCallback!!)
|
||||||
|
|
||||||
setOnTouchListener { _, motionEvent ->
|
setOnTouchListener { _, motionEvent ->
|
||||||
val ret = gestureDetector?.onTouchEvent(motionEvent) ?: false
|
val ret = gestureDetector?.onTouchEvent(motionEvent) ?: false
|
||||||
|
@ -128,8 +128,8 @@ class NowPlayingView : MaterialCardView {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFling(
|
override fun onFling(
|
||||||
firstMotionEvent: MotionEvent?,
|
firstMotionEvent: MotionEvent,
|
||||||
secondMotionEvent: MotionEvent?,
|
secondMotionEvent: MotionEvent,
|
||||||
velocityX: Float,
|
velocityX: Float,
|
||||||
velocityY: Float
|
velocityY: Float
|
||||||
): Boolean {
|
): Boolean {
|
||||||
|
@ -195,7 +195,7 @@ class NowPlayingView : MaterialCardView {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSingleTapUp(e: MotionEvent?): Boolean {
|
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
||||||
layoutParams.let {
|
layoutParams.let {
|
||||||
if (height != minHeight) return true
|
if (height != minHeight) return true
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
app:tabSelectedTextColor="@color/controlColor"
|
app:tabSelectedTextColor="@color/controlColor"
|
||||||
app:tabTextColor="@color/colorPrimary" />
|
app:tabTextColor="@color/colorPrimary" />
|
||||||
|
|
||||||
<androidx.viewpager.widget.ViewPager
|
<androidx.viewpager2.widget.ViewPager2
|
||||||
android:id="@+id/pager"
|
android:id="@+id/pager"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
|
|
@ -27,6 +27,7 @@ import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import strikt.api.expectThat
|
import strikt.api.expectThat
|
||||||
import strikt.api.expectThrows
|
import strikt.api.expectThrows
|
||||||
|
import strikt.assertions.isA
|
||||||
import strikt.assertions.isEqualTo
|
import strikt.assertions.isEqualTo
|
||||||
import strikt.assertions.isFalse
|
import strikt.assertions.isFalse
|
||||||
import strikt.assertions.isNotNull
|
import strikt.assertions.isNotNull
|
||||||
|
@ -282,7 +283,7 @@ class OAuthTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `authorize() should start activity for result`() {
|
fun `authorizeIntent() should return an Intent`() {
|
||||||
|
|
||||||
mockkStatic(PowerPreference::class)
|
mockkStatic(PowerPreference::class)
|
||||||
every { PowerPreference.getFileByName(any()) } returns mockPreference
|
every { PowerPreference.getFileByName(any()) } returns mockPreference
|
||||||
|
@ -302,9 +303,7 @@ class OAuthTest {
|
||||||
|
|
||||||
val activity = mockk<Activity>(relaxed = true)
|
val activity = mockk<Activity>(relaxed = true)
|
||||||
|
|
||||||
oAuth.authorize(activity)
|
expectThat(oAuth.authorizeIntent(activity)).isNotNull().isA<Intent>()
|
||||||
|
|
||||||
verify { activity.startActivityForResult(mockkIntent, 0) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <T> deserializeJson(
|
private fun <T> deserializeJson(
|
||||||
|
|
Loading…
Reference in New Issue