Add basic playback functionaliity

Implement ExoPlayer in a basic form so that songs can be actually played.
This commit is contained in:
OxygenCobalt 2020-10-25 18:18:04 -06:00
parent 32fe24e001
commit 25142bba48
16 changed files with 138 additions and 17 deletions

View file

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

View file

@ -3,6 +3,8 @@
package="org.oxycblt.auxio">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
android:allowBackup="true"
@ -21,6 +23,10 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name=".playback.PlaybackService"
android:icon="@drawable/ic_launcher_foreground"
android:description="@string/description_service_playback"
android:stopWithTask="false"/>
</application>
</manifest>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<Song>()
val currentSong: LiveData<Song> 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 <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(PlaybackViewModel::class.java)) {
return PlaybackViewModel(context) as T
}
throw IllegalArgumentException("Unknown ViewModel class.")
}
}
}

View file

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

View file

@ -3,7 +3,8 @@
android:width="32dp"
android:height="32dp"
android:viewportWidth="24"
android:viewportHeight="24">
android:viewportHeight="24"
android:tint="?android:attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="m 7.5,6 h 3 v 12 h -3 z m 9,0 h -3 v 12 h 3 z" />

View file

@ -3,7 +3,8 @@
android:width="32dp"
android:height="32dp"
android:viewportWidth="24"
android:viewportHeight="24">
android:viewportHeight="24"
android:tint="?android:attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M8.25,6L8.25,12L18.75,12L18.75,12ZM8.25,18L8.25,12L18.75,12L18.75,12Z" />

View file

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

View file

@ -1,13 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_play"
android:title="@string/label_play"
android:icon="@drawable/ic_play"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_shuffle"
android:icon="@drawable/ic_shuffle"
android:title="@string/label_shuffle"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_play"
android:title="@string/label_play"
app:showAsAction="never" />
</menu>

View file

@ -47,6 +47,7 @@
<string name="description_skip_prev">Skip to last song</string>
<string name="description_shuffle_on">Turn shuffle on</string>
<string name="description_shuffle_off">Turn shuffle off</string>
<string name="description_service_playback">The music playback service for Auxio</string>
<!-- Placeholder Namespace | Placeholder values -->
<string name="placeholder_genre">Unknown Genre</string>