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