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 { kotlinOptions {
jvmTarget = "1.8" jvmTarget = "1.8"
} }
compileOptions {
targetCompatibility JavaVersion.VERSION_1_8
}
} }
configurations { configurations {
@ -76,6 +80,9 @@ dependencies {
// Lint // Lint
ktlint "com.pinterest:ktlint:0.37.2" ktlint "com.pinterest:ktlint:0.37.2"
// ExoPlayer
implementation 'com.google.android.exoplayer:exoplayer-core:2.12.1'
// Memory Leak checking // Memory Leak checking
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4' debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4'
} }

View file

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

View file

@ -23,7 +23,9 @@ import org.oxycblt.auxio.theme.getTransparentAccent
import org.oxycblt.auxio.theme.toColor import org.oxycblt.auxio.theme.toColor
class MainFragment : Fragment() { 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 shownFragments = listOf(0, 1)
private val tabIcons = listOf( private val tabIcons = listOf(

View file

@ -22,7 +22,9 @@ class AlbumDetailFragment : Fragment() {
private val args: AlbumDetailFragmentArgs by navArgs() private val args: AlbumDetailFragmentArgs by navArgs()
private val detailModel: DetailViewModel by activityViewModels() private val detailModel: DetailViewModel by activityViewModels()
private val playbackModel: PlaybackViewModel by activityViewModels() private val playbackModel: PlaybackViewModel by activityViewModels {
PlaybackViewModel.Factory(requireContext())
}
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,

View file

@ -20,7 +20,9 @@ import org.oxycblt.auxio.theme.disable
class ArtistDetailFragment : Fragment() { class ArtistDetailFragment : Fragment() {
private val args: ArtistDetailFragmentArgs by navArgs() private val args: ArtistDetailFragmentArgs by navArgs()
private val detailModel: DetailViewModel by activityViewModels() private val detailModel: DetailViewModel by activityViewModels()
private val playbackModel: PlaybackViewModel by activityViewModels() private val playbackModel: PlaybackViewModel by activityViewModels {
PlaybackViewModel.Factory(requireContext())
}
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,

View file

@ -21,7 +21,9 @@ class GenreDetailFragment : Fragment() {
private val args: GenreDetailFragmentArgs by navArgs() private val args: GenreDetailFragmentArgs by navArgs()
private val detailModel: DetailViewModel by activityViewModels() private val detailModel: DetailViewModel by activityViewModels()
private val playbackModel: PlaybackViewModel by activityViewModels() private val playbackModel: PlaybackViewModel by activityViewModels {
PlaybackViewModel.Factory(requireContext())
}
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,

View file

@ -35,7 +35,9 @@ import org.oxycblt.auxio.theme.resolveAttr
class LibraryFragment : Fragment(), SearchView.OnQueryTextListener { class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
private val libraryModel: LibraryViewModel by activityViewModels() private val libraryModel: LibraryViewModel by activityViewModels()
private val playbackModel: PlaybackViewModel by activityViewModels() private val playbackModel: PlaybackViewModel by activityViewModels {
PlaybackViewModel.Factory(requireContext())
}
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, 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 // TODO: Add a swipe-to-next-track function using a ViewPager
class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener { 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 // TODO: Implement nav to artists/albums
override fun onCreateView( 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 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 android.util.Log
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel 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.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.BaseModel import org.oxycblt.auxio.music.BaseModel
@ -15,12 +22,12 @@ import org.oxycblt.auxio.music.toDuration
import kotlin.random.Random import kotlin.random.Random
import kotlin.random.Random.Default.nextLong import kotlin.random.Random.Default.nextLong
// TODO: Adding to Queue // TODO: User managed queue
// TODO: Add the playback service itself // TODO: Add the playback service itself
// TODO: Add loop control [From playback] // TODO: Add loop control [From playback]
// TODO: Implement persistence through Bundles [I want to keep my shuffles, okay?] // 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. // 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>() private val mCurrentSong = MutableLiveData<Song>()
val currentSong: LiveData<Song> get() = mCurrentSong val currentSong: LiveData<Song> get() = mCurrentSong
@ -53,6 +60,25 @@ class PlaybackViewModel : ViewModel() {
private var mCanAnimate = false private var mCanAnimate = false
val canAnimate: Boolean get() = mCanAnimate 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 // Formatted variants of the duration
val formattedCurrentDuration = Transformations.map(mCurrentDuration) { val formattedCurrentDuration = Transformations.map(mCurrentDuration) {
it.toDuration() it.toDuration()
@ -69,7 +95,6 @@ class PlaybackViewModel : ViewModel() {
// Update the current song while changing the queue mode. // Update the current song while changing the queue mode.
fun update(song: Song, mode: PlaybackMode) { fun update(song: Song, mode: PlaybackMode) {
// Auxio doesn't support playing songs while swapping the mode to GENRE, as its impossible // Auxio doesn't support playing songs while swapping the mode to GENRE, as its impossible
// to determine what genre a song has. // to determine what genre a song has.
if (mode == PlaybackMode.IN_GENRE) { if (mode == PlaybackMode.IN_GENRE) {
@ -312,6 +337,8 @@ class PlaybackViewModel : ViewModel() {
if (!mIsPlaying.value!!) { if (!mIsPlaying.value!!) {
mIsPlaying.value = true mIsPlaying.value = true
} }
playbackService.playSong(song)
} }
// Generate a new shuffled queue. // Generate a new shuffled queue.
@ -383,4 +410,21 @@ class PlaybackViewModel : ViewModel() {
return final 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 import org.oxycblt.auxio.theme.applyDivider
class SongsFragment : Fragment() { class SongsFragment : Fragment() {
private val playbackModel: PlaybackViewModel by activityViewModels() private val playbackModel: PlaybackViewModel by activityViewModels {
PlaybackViewModel.Factory(requireContext())
}
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,

View file

@ -3,7 +3,8 @@
android:width="32dp" android:width="32dp"
android:height="32dp" android:height="32dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24"
android:tint="?android:attr/colorControlNormal">
<path <path
android:fillColor="@android:color/white" 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" /> 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:width="32dp"
android:height="32dp" android:height="32dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24"
android:tint="?android:attr/colorControlNormal">
<path <path
android:fillColor="@android:color/white" android:fillColor="@android:color/white"
android:pathData="M8.25,6L8.25,12L18.75,12L18.75,12ZM8.25,18L8.25,12L18.75,12L18.75,12Z" /> 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:layout_height="?android:attr/actionBarSize"
android:background="?android:attr/windowBackground" android:background="?android:attr/windowBackground"
android:elevation="@dimen/elevation_normal" android:elevation="@dimen/elevation_normal"
app:popupTheme="@style/AppThemeOverlay.Popup"
app:menu="@menu/menu_detail" app:menu="@menu/menu_detail"
app:titleTextAppearance="@style/TextAppearance.Toolbar.Header" app:titleTextAppearance="@style/TextAppearance.Toolbar.Header"
app:navigationIcon="@drawable/ic_back" app:navigationIcon="@drawable/ic_back"

View file

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

View file

@ -47,6 +47,7 @@
<string name="description_skip_prev">Skip to last song</string> <string name="description_skip_prev">Skip to last song</string>
<string name="description_shuffle_on">Turn shuffle on</string> <string name="description_shuffle_on">Turn shuffle on</string>
<string name="description_shuffle_off">Turn shuffle off</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 --> <!-- Placeholder Namespace | Placeholder values -->
<string name="placeholder_genre">Unknown Genre</string> <string name="placeholder_genre">Unknown Genre</string>