diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f93b57f..4ba0f2a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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 } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b6e4126..24462a3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + @@ -22,7 +21,8 @@ android:name=".activities.SplashActivity" android:launchMode="singleInstance" android:noHistory="true" - android:screenOrientation="portrait"> + android:screenOrientation="portrait" + android:exported="true"> @@ -61,7 +61,8 @@ + android:foregroundServiceType="mediaPlayback" + android:exported="false"> @@ -80,7 +81,8 @@ - + diff --git a/app/src/main/java/audio/funkwhale/ffa/FFA.kt b/app/src/main/java/audio/funkwhale/ffa/FFA.kt index 0b8fe9d..29af3b7 100644 --- a/app/src/main/java/audio/funkwhale/ffa/FFA.kt +++ b/app/src/main/java/audio/funkwhale/ffa/FFA.kt @@ -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 = BroadcastChannel(10) - val commandBus: BroadcastChannel = BroadcastChannel(10) - val requestBus: BroadcastChannel = BroadcastChannel(10) - val progressBus: BroadcastChannel> = ConflatedBroadcastChannel() - override fun onCreate() { super.onCreate() diff --git a/app/src/main/java/audio/funkwhale/ffa/activities/LoginActivity.kt b/app/src/main/java/audio/funkwhale/ffa/activities/LoginActivity.kt index 333123f..914be0f 100644 --- a/app/src/main/java/audio/funkwhale/ffa/activities/LoginActivity.kt +++ b/app/src/main/java/audio/funkwhale/ffa/activities/LoginActivity.kt @@ -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)) } } diff --git a/app/src/main/java/audio/funkwhale/ffa/activities/MainActivity.kt b/app/src/main/java/audio/funkwhale/ffa/activities/MainActivity.kt index 14d8939..c65a437 100644 --- a/app/src/main/java/audio/funkwhale/ffa/activities/MainActivity.kt +++ b/app/src/main/java/audio/funkwhale/ffa/activities/MainActivity.kt @@ -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)) diff --git a/app/src/main/java/audio/funkwhale/ffa/activities/SettingsActivity.kt b/app/src/main/java/audio/funkwhale/ffa/activities/SettingsActivity.kt index 2e990d5..28c11a5 100644 --- a/app/src/main/java/audio/funkwhale/ffa/activities/SettingsActivity.kt +++ b/app/src/main/java/audio/funkwhale/ffa/activities/SettingsActivity.kt @@ -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" -> { diff --git a/app/src/main/java/audio/funkwhale/ffa/adapters/BrowseTabsAdapter.kt b/app/src/main/java/audio/funkwhale/ffa/adapters/BrowseTabsAdapter.kt index 272f24f..b8cb5ba 100644 --- a/app/src/main/java/audio/funkwhale/ffa/adapters/BrowseTabsAdapter.kt +++ b/app/src/main/java/audio/funkwhale/ffa/adapters/BrowseTabsAdapter.kt @@ -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() - 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) diff --git a/app/src/main/java/audio/funkwhale/ffa/fragments/BrowseFragment.kt b/app/src/main/java/audio/funkwhale/ffa/fragments/BrowseFragment.kt index f0a78a1..996c622 100644 --- a/app/src/main/java/audio/funkwhale/ffa/fragments/BrowseFragment.kt +++ b/app/src/main/java/audio/funkwhale/ffa/fragments/BrowseFragment.kt @@ -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() } } diff --git a/app/src/main/java/audio/funkwhale/ffa/koin/Modules.kt b/app/src/main/java/audio/funkwhale/ffa/koin/Modules.kt index d06ac33..0569a52 100644 --- a/app/src/main/java/audio/funkwhale/ffa/koin/Modules.kt +++ b/app/src/main/java/audio/funkwhale/ffa/koin/Modules.kt @@ -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(named("exoDatabase")) { - ExoDatabaseProvider(context) + StandaloneDatabaseProvider(context) } single { diff --git a/app/src/main/java/audio/funkwhale/ffa/playback/MediaControlsManager.kt b/app/src/main/java/audio/funkwhale/ffa/playback/MediaControlsManager.kt index 36b5302..9e1faee 100644 --- a/app/src/main/java/audio/funkwhale/ffa/playback/MediaControlsManager.kt +++ b/app/src/main/java/audio/funkwhale/ffa/playback/MediaControlsManager.kt @@ -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) } } diff --git a/app/src/main/java/audio/funkwhale/ffa/playback/MediaSession.kt b/app/src/main/java/audio/funkwhale/ffa/playback/MediaSession.kt index 8898309..ce3d88c 100644 --- a/app/src/main/java/audio/funkwhale/ffa/playback/MediaSession.kt +++ b/app/src/main/java/audio/funkwhale/ffa/playback/MediaSession.kt @@ -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) } diff --git a/app/src/main/java/audio/funkwhale/ffa/playback/PinService.kt b/app/src/main/java/audio/funkwhale/ffa/playback/PinService.kt index f3bd1ce..3b5becf 100644 --- a/app/src/main/java/audio/funkwhale/ffa/playback/PinService.kt +++ b/app/src/main/java/audio/funkwhale/ffa/playback/PinService.kt @@ -80,14 +80,20 @@ class PinService : DownloadService(AppContext.NOTIFICATION_DOWNLOADS) { override fun getScheduler(): Scheduler? = null - override fun getForegroundNotification(downloads: MutableList): Notification { + override fun getForegroundNotification( + downloads: MutableList, + 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() diff --git a/app/src/main/java/audio/funkwhale/ffa/playback/PlayerService.kt b/app/src/main/java/audio/funkwhale/ffa/playback/PlayerService.kt index 9c74da5..6b1c606 100644 --- a/app/src/main/java/audio/funkwhale/ffa/playback/PlayerService.kt +++ b/app/src/main/java/audio/funkwhale/ffa/playback/PlayerService.kt @@ -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())) } diff --git a/app/src/main/java/audio/funkwhale/ffa/playback/QueueManager.kt b/app/src/main/java/audio/funkwhale/ffa/playback/QueueManager.kt index fe9bf31..821b860 100644 --- a/app/src/main/java/audio/funkwhale/ffa/playback/QueueManager.kt +++ b/app/src/main/java/audio/funkwhale/ffa/playback/QueueManager.kt @@ -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) } diff --git a/app/src/main/java/audio/funkwhale/ffa/repositories/SearchRepository.kt b/app/src/main/java/audio/funkwhale/ffa/repositories/SearchRepository.kt index ee32d64..60021e1 100644 --- a/app/src/main/java/audio/funkwhale/ffa/repositories/SearchRepository.kt +++ b/app/src/main/java/audio/funkwhale/ffa/repositories/SearchRepository.kt @@ -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 diff --git a/app/src/main/java/audio/funkwhale/ffa/repositories/TracksRepository.kt b/app/src/main/java/audio/funkwhale/ffa/repositories/TracksRepository.kt index 7858bf0..7a224c6 100644 --- a/app/src/main/java/audio/funkwhale/ffa/repositories/TracksRepository.kt +++ b/app/src/main/java/audio/funkwhale/ffa/repositories/TracksRepository.kt @@ -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 diff --git a/app/src/main/java/audio/funkwhale/ffa/utils/Bus.kt b/app/src/main/java/audio/funkwhale/ffa/utils/Bus.kt index 49e836c..83b3ac1 100644 --- a/app/src/main/java/audio/funkwhale/ffa/utils/Bus.kt +++ b/app/src/main/java/audio/funkwhale/ffa/utils/Bus.kt @@ -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() + 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() + 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() + var requests = _requests.asSharedFlow() fun send(request: Request): Channel { return Channel().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 Channel.wait(): T? { diff --git a/app/src/main/java/audio/funkwhale/ffa/utils/Extensions.kt b/app/src/main/java/audio/funkwhale/ffa/utils/Extensions.kt index 7fb8123..fbd5e81 100644 --- a/app/src/main/java/audio/funkwhale/ffa/utils/Extensions.kt +++ b/app/src/main/java/audio/funkwhale/ffa/utils/Extensions.kt @@ -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 diff --git a/app/src/main/java/audio/funkwhale/ffa/utils/OAuth.kt b/app/src/main/java/audio/funkwhale/ffa/utils/OAuth.kt index 9e21669..19560ed 100644 --- a/app/src/main/java/audio/funkwhale/ffa/utils/OAuth.kt +++ b/app/src/main/java/audio/funkwhale/ffa/utils/OAuth.kt @@ -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) } } diff --git a/app/src/main/java/audio/funkwhale/ffa/views/NowPlayingView.kt b/app/src/main/java/audio/funkwhale/ffa/views/NowPlayingView.kt index d4133b0..7ad7374 100644 --- a/app/src/main/java/audio/funkwhale/ffa/views/NowPlayingView.kt +++ b/app/src/main/java/audio/funkwhale/ffa/views/NowPlayingView.kt @@ -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 diff --git a/app/src/main/res/layout/fragment_browse.xml b/app/src/main/res/layout/fragment_browse.xml index b58b4d1..afc7ccb 100644 --- a/app/src/main/res/layout/fragment_browse.xml +++ b/app/src/main/res/layout/fragment_browse.xml @@ -17,7 +17,7 @@ app:tabSelectedTextColor="@color/controlColor" app:tabTextColor="@color/colorPrimary" /> - (relaxed = true) - oAuth.authorize(activity) - - verify { activity.startActivityForResult(mockkIntent, 0) } + expectThat(oAuth.authorizeIntent(activity)).isNotNull().isA() } private fun deserializeJson(