Add basic playback functionaliity
Implement ExoPlayer in a basic form so that songs can be actually played.
This commit is contained in:
parent
32fe24e001
commit
25142bba48
16 changed files with 138 additions and 17 deletions
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue