From 25142bba48a6e0c064f7ce47f0ca160494929ae0 Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Sun, 25 Oct 2020 18:18:04 -0600 Subject: [PATCH] Add basic playback functionaliity Implement ExoPlayer in a basic form so that songs can be actually played. --- app/build.gradle | 7 +++ app/src/main/AndroidManifest.xml | 6 +++ .../java/org/oxycblt/auxio/MainFragment.kt | 4 +- .../auxio/detail/AlbumDetailFragment.kt | 4 +- .../auxio/detail/ArtistDetailFragment.kt | 4 +- .../auxio/detail/GenreDetailFragment.kt | 4 +- .../oxycblt/auxio/library/LibraryFragment.kt | 4 +- .../auxio/playback/PlaybackFragment.kt | 4 +- .../oxycblt/auxio/playback/PlaybackService.kt | 47 +++++++++++++++++ .../auxio/playback/PlaybackViewModel.kt | 50 +++++++++++++++++-- .../org/oxycblt/auxio/songs/SongsFragment.kt | 4 +- app/src/main/res/drawable/ic_pause.xml | 3 +- app/src/main/res/drawable/ic_play.xml | 3 +- .../res/layout/fragment_artist_detail.xml | 1 - app/src/main/res/menu/menu_detail.xml | 9 ++-- app/src/main/res/values/strings.xml | 1 + 16 files changed, 138 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/org/oxycblt/auxio/playback/PlaybackService.kt diff --git a/app/build.gradle b/app/build.gradle index 174c85e06..87966773e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -32,6 +32,10 @@ android { kotlinOptions { jvmTarget = "1.8" } + + compileOptions { + targetCompatibility JavaVersion.VERSION_1_8 + } } configurations { @@ -76,6 +80,9 @@ dependencies { // Lint ktlint "com.pinterest:ktlint:0.37.2" + // ExoPlayer + implementation 'com.google.android.exoplayer:exoplayer-core:2.12.1' + // Memory Leak checking debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 203f183e5..3b33222b0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,8 @@ package="org.oxycblt.auxio"> + + + \ No newline at end of file diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index da6cd0e6d..8ed3b495c 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -23,7 +23,9 @@ import org.oxycblt.auxio.theme.getTransparentAccent import org.oxycblt.auxio.theme.toColor class MainFragment : Fragment() { - private val playbackModel: PlaybackViewModel by activityViewModels() + private val playbackModel: PlaybackViewModel by activityViewModels { + PlaybackViewModel.Factory(requireContext()) + } private val shownFragments = listOf(0, 1) private val tabIcons = listOf( diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt index af8d909f7..8dde340d8 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -22,7 +22,9 @@ class AlbumDetailFragment : Fragment() { private val args: AlbumDetailFragmentArgs by navArgs() private val detailModel: DetailViewModel by activityViewModels() - private val playbackModel: PlaybackViewModel by activityViewModels() + private val playbackModel: PlaybackViewModel by activityViewModels { + PlaybackViewModel.Factory(requireContext()) + } override fun onCreateView( inflater: LayoutInflater, diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt index 7c133d152..613183667 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -20,7 +20,9 @@ import org.oxycblt.auxio.theme.disable class ArtistDetailFragment : Fragment() { private val args: ArtistDetailFragmentArgs by navArgs() private val detailModel: DetailViewModel by activityViewModels() - private val playbackModel: PlaybackViewModel by activityViewModels() + private val playbackModel: PlaybackViewModel by activityViewModels { + PlaybackViewModel.Factory(requireContext()) + } override fun onCreateView( inflater: LayoutInflater, diff --git a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt index 0d7418022..b521998a5 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -21,7 +21,9 @@ class GenreDetailFragment : Fragment() { private val args: GenreDetailFragmentArgs by navArgs() private val detailModel: DetailViewModel by activityViewModels() - private val playbackModel: PlaybackViewModel by activityViewModels() + private val playbackModel: PlaybackViewModel by activityViewModels { + PlaybackViewModel.Factory(requireContext()) + } override fun onCreateView( inflater: LayoutInflater, diff --git a/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt b/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt index 91127661d..02a42ba77 100644 --- a/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt @@ -35,7 +35,9 @@ import org.oxycblt.auxio.theme.resolveAttr class LibraryFragment : Fragment(), SearchView.OnQueryTextListener { private val libraryModel: LibraryViewModel by activityViewModels() - private val playbackModel: PlaybackViewModel by activityViewModels() + private val playbackModel: PlaybackViewModel by activityViewModels { + PlaybackViewModel.Factory(requireContext()) + } override fun onCreateView( inflater: LayoutInflater, diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt index 72d2c3109..c157dafac 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt @@ -22,7 +22,9 @@ import org.oxycblt.auxio.theme.toColor // TODO: Add a swipe-to-next-track function using a ViewPager class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener { - private val playbackModel: PlaybackViewModel by activityViewModels() + private val playbackModel: PlaybackViewModel by activityViewModels { + PlaybackViewModel.Factory(requireActivity().application) + } // TODO: Implement nav to artists/albums override fun onCreateView( diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackService.kt new file mode 100644 index 000000000..61fba8d3b --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackService.kt @@ -0,0 +1,47 @@ +package org.oxycblt.auxio.playback + +import android.app.Service +import android.content.Intent +import android.os.Binder +import android.os.Build +import android.os.IBinder +import com.google.android.exoplayer2.MediaItem +import com.google.android.exoplayer2.Player +import com.google.android.exoplayer2.SimpleExoPlayer +import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.music.toURI + +class PlaybackService : Service(), Player.EventListener { + private val player: SimpleExoPlayer by lazy { + val p = SimpleExoPlayer.Builder(applicationContext).build() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + p.experimentalSetOffloadSchedulingEnabled(true) + } + p.addListener(this) + p + } + + private val mBinder = LocalBinder() + + override fun onBind(intent: Intent): IBinder { + return mBinder + } + + override fun onDestroy() { + super.onDestroy() + + player.release() + } + + fun playSong(song: Song) { + val item = MediaItem.fromUri(song.id.toURI()) + + player.setMediaItem(item) + player.prepare() + player.play() + } + + inner class LocalBinder : Binder() { + fun getService() = this@PlaybackService + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt index d398e1537..e25d0fb10 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -1,10 +1,17 @@ package org.oxycblt.auxio.playback +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.os.IBinder import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Transformations import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.google.android.exoplayer2.Player import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.BaseModel @@ -15,12 +22,12 @@ import org.oxycblt.auxio.music.toDuration import kotlin.random.Random import kotlin.random.Random.Default.nextLong -// TODO: Adding to Queue +// TODO: User managed queue // TODO: Add the playback service itself // TODO: Add loop control [From playback] // TODO: Implement persistence through Bundles [I want to keep my shuffles, okay?] // A ViewModel that acts as an intermediary between PlaybackService and the Playback Fragments. -class PlaybackViewModel : ViewModel() { +class PlaybackViewModel(private val context: Context) : ViewModel(), Player.EventListener { private val mCurrentSong = MutableLiveData() val currentSong: LiveData get() = mCurrentSong @@ -53,6 +60,25 @@ class PlaybackViewModel : ViewModel() { private var mCanAnimate = false val canAnimate: Boolean get() = mCanAnimate + private lateinit var playbackService: PlaybackService + private var playbackIntent: Intent + + private val connection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName, binder: IBinder) { + playbackService = (binder as PlaybackService.LocalBinder).getService() + } + + override fun onServiceDisconnected(name: ComponentName) { + Log.d(this::class.simpleName, "Service disconnected.") + } + } + + init { + playbackIntent = Intent(context, PlaybackService::class.java).also { + context.bindService(it, connection, Context.BIND_AUTO_CREATE) + } + } + // Formatted variants of the duration val formattedCurrentDuration = Transformations.map(mCurrentDuration) { it.toDuration() @@ -69,7 +95,6 @@ class PlaybackViewModel : ViewModel() { // Update the current song while changing the queue mode. fun update(song: Song, mode: PlaybackMode) { - // Auxio doesn't support playing songs while swapping the mode to GENRE, as its impossible // to determine what genre a song has. if (mode == PlaybackMode.IN_GENRE) { @@ -312,6 +337,8 @@ class PlaybackViewModel : ViewModel() { if (!mIsPlaying.value!!) { mIsPlaying.value = true } + + playbackService.playSong(song) } // Generate a new shuffled queue. @@ -383,4 +410,21 @@ class PlaybackViewModel : ViewModel() { return final } + + override fun onCleared() { + super.onCleared() + + context.unbindService(connection) + } + + class Factory(private val context: Context) : ViewModelProvider.Factory { + @Suppress("unchecked_cast") + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(PlaybackViewModel::class.java)) { + return PlaybackViewModel(context) as T + } + + throw IllegalArgumentException("Unknown ViewModel class.") + } + } } diff --git a/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt b/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt index 78fb66cc9..9c8b85991 100644 --- a/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt @@ -15,7 +15,9 @@ import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.theme.applyDivider class SongsFragment : Fragment() { - private val playbackModel: PlaybackViewModel by activityViewModels() + private val playbackModel: PlaybackViewModel by activityViewModels { + PlaybackViewModel.Factory(requireContext()) + } override fun onCreateView( inflater: LayoutInflater, diff --git a/app/src/main/res/drawable/ic_pause.xml b/app/src/main/res/drawable/ic_pause.xml index f5f4c3387..60ef4d1d3 100644 --- a/app/src/main/res/drawable/ic_pause.xml +++ b/app/src/main/res/drawable/ic_pause.xml @@ -3,7 +3,8 @@ android:width="32dp" android:height="32dp" android:viewportWidth="24" - android:viewportHeight="24"> + android:viewportHeight="24" + android:tint="?android:attr/colorControlNormal"> diff --git a/app/src/main/res/drawable/ic_play.xml b/app/src/main/res/drawable/ic_play.xml index 832dec261..3dfcbc471 100644 --- a/app/src/main/res/drawable/ic_play.xml +++ b/app/src/main/res/drawable/ic_play.xml @@ -3,7 +3,8 @@ android:width="32dp" android:height="32dp" android:viewportWidth="24" - android:viewportHeight="24"> + android:viewportHeight="24" + android:tint="?android:attr/colorControlNormal"> diff --git a/app/src/main/res/layout/fragment_artist_detail.xml b/app/src/main/res/layout/fragment_artist_detail.xml index 1997bd23d..6fd7866d1 100644 --- a/app/src/main/res/layout/fragment_artist_detail.xml +++ b/app/src/main/res/layout/fragment_artist_detail.xml @@ -30,7 +30,6 @@ android:layout_height="?android:attr/actionBarSize" android:background="?android:attr/windowBackground" android:elevation="@dimen/elevation_normal" - app:popupTheme="@style/AppThemeOverlay.Popup" app:menu="@menu/menu_detail" app:titleTextAppearance="@style/TextAppearance.Toolbar.Header" app:navigationIcon="@drawable/ic_back" diff --git a/app/src/main/res/menu/menu_detail.xml b/app/src/main/res/menu/menu_detail.xml index 118d3eb14..80c4741a8 100644 --- a/app/src/main/res/menu/menu_detail.xml +++ b/app/src/main/res/menu/menu_detail.xml @@ -1,13 +1,14 @@ + - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 66e334a28..f3006f204 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -47,6 +47,7 @@ Skip to last song Turn shuffle on Turn shuffle off + The music playback service for Auxio Unknown Genre