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 {
|
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'
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
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.")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue