Delete older music service
This commit is contained in:
parent
709c2205e5
commit
88b0b1aba4
2 changed files with 0 additions and 1127 deletions
|
@ -1,270 +0,0 @@
|
|||
package com.simplemobiletools.musicplayer.helpers
|
||||
|
||||
import android.app.Application
|
||||
import android.media.AudioAttributes
|
||||
import android.media.AudioManager
|
||||
import android.media.MediaPlayer
|
||||
import android.net.Uri
|
||||
import android.os.PowerManager
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.media.AudioAttributesCompat
|
||||
import androidx.media.AudioFocusRequestCompat
|
||||
import androidx.media.AudioManagerCompat
|
||||
import com.simplemobiletools.musicplayer.extensions.config
|
||||
|
||||
class MultiPlayer(private val app: Application, private val callbacks: PlaybackCallbacks) : MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener {
|
||||
private var mCurrentMediaPlayer = MediaPlayer()
|
||||
private var mNextMediaPlayer: MediaPlayer? = null
|
||||
|
||||
var isInitialized: Boolean = false
|
||||
|
||||
private val audioManager: AudioManager? = app.getSystemService()
|
||||
private var isPausedByTransientLossOfFocus = false
|
||||
private val audioFocusListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
|
||||
when (focusChange) {
|
||||
AudioManager.AUDIOFOCUS_GAIN -> {
|
||||
if (!isPlaying() && isPausedByTransientLossOfFocus) {
|
||||
start()
|
||||
callbacks.onPlayStateChanged()
|
||||
isPausedByTransientLossOfFocus = false
|
||||
}
|
||||
setVolume(Volume.NORMAL)
|
||||
}
|
||||
|
||||
AudioManager.AUDIOFOCUS_LOSS -> {
|
||||
pause()
|
||||
callbacks.onPlayStateChanged()
|
||||
}
|
||||
|
||||
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
||||
val wasPlaying = isPlaying()
|
||||
pause()
|
||||
callbacks.onPlayStateChanged()
|
||||
isPausedByTransientLossOfFocus = wasPlaying
|
||||
}
|
||||
|
||||
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
|
||||
setVolume(Volume.DUCK)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var audioFocusRequest: AudioFocusRequestCompat? = null
|
||||
|
||||
init {
|
||||
mCurrentMediaPlayer.setWakeMode(app, PowerManager.PARTIAL_WAKE_LOCK)
|
||||
}
|
||||
|
||||
fun getAudioSessionId(): Int = mCurrentMediaPlayer.audioSessionId
|
||||
|
||||
fun isPlaying(): Boolean = isInitialized && mCurrentMediaPlayer.isPlaying
|
||||
|
||||
fun setDataSource(trackUri: Uri) {
|
||||
isInitialized = false
|
||||
setDataSourceImpl(mCurrentMediaPlayer, trackUri) { result ->
|
||||
if (result.isFailure) {
|
||||
throw result.exceptionOrNull()!!
|
||||
}
|
||||
isInitialized = true
|
||||
setNextDataSource(null)
|
||||
callbacks.onPrepared()
|
||||
}
|
||||
}
|
||||
|
||||
fun setNextDataSource(trackUri: Uri?, onPrepared: (() -> Unit)? = null) {
|
||||
try {
|
||||
mCurrentMediaPlayer.setNextMediaPlayer(null)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// Next media player is current one, continuing
|
||||
} catch (e: IllegalStateException) {
|
||||
return
|
||||
}
|
||||
releaseNextPlayer()
|
||||
if (trackUri == null) {
|
||||
return
|
||||
}
|
||||
if (app.config.gaplessPlayback) {
|
||||
mNextMediaPlayer = MediaPlayer()
|
||||
mNextMediaPlayer!!.setWakeMode(app, PowerManager.PARTIAL_WAKE_LOCK)
|
||||
mNextMediaPlayer!!.audioSessionId = mCurrentMediaPlayer.audioSessionId
|
||||
setDataSourceImpl(mNextMediaPlayer!!, trackUri) { result ->
|
||||
if (result.isSuccess) {
|
||||
try {
|
||||
mCurrentMediaPlayer.setNextMediaPlayer(mNextMediaPlayer)
|
||||
onPrepared?.invoke()
|
||||
} catch (e: IllegalArgumentException) {
|
||||
releaseNextPlayer()
|
||||
} catch (e: IllegalStateException) {
|
||||
releaseNextPlayer()
|
||||
}
|
||||
} else {
|
||||
releaseNextPlayer()
|
||||
throw result.exceptionOrNull()!!
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun start(): Boolean {
|
||||
requestFocus()
|
||||
return try {
|
||||
mCurrentMediaPlayer.start()
|
||||
true
|
||||
} catch (e: IllegalStateException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
abandonFocus()
|
||||
mCurrentMediaPlayer.reset()
|
||||
isInitialized = false
|
||||
}
|
||||
|
||||
fun release() {
|
||||
stop()
|
||||
mCurrentMediaPlayer.release()
|
||||
mNextMediaPlayer?.release()
|
||||
}
|
||||
|
||||
fun pause(): Boolean {
|
||||
if (!isPlaying()) {
|
||||
return false
|
||||
}
|
||||
|
||||
return try {
|
||||
mCurrentMediaPlayer.pause()
|
||||
true
|
||||
} catch (e: IllegalStateException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun duration(): Int {
|
||||
return if (!this.isInitialized) {
|
||||
-1
|
||||
} else try {
|
||||
mCurrentMediaPlayer.duration
|
||||
} catch (e: IllegalStateException) {
|
||||
-1
|
||||
}
|
||||
}
|
||||
|
||||
fun position(): Int {
|
||||
return if (!this.isInitialized) {
|
||||
-1
|
||||
} else try {
|
||||
mCurrentMediaPlayer.currentPosition
|
||||
} catch (e: IllegalStateException) {
|
||||
-1
|
||||
}
|
||||
}
|
||||
|
||||
fun seek(whereto: Int): Int {
|
||||
return try {
|
||||
mCurrentMediaPlayer.seekTo(whereto)
|
||||
whereto
|
||||
} catch (e: IllegalStateException) {
|
||||
-1
|
||||
}
|
||||
}
|
||||
|
||||
fun setVolume(vol: Float): Boolean {
|
||||
return try {
|
||||
mCurrentMediaPlayer.setVolume(vol, vol)
|
||||
true
|
||||
} catch (e: IllegalStateException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(mp: MediaPlayer, what: Int, extra: Int): Boolean {
|
||||
isInitialized = false
|
||||
mCurrentMediaPlayer.reset()
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onCompletion(mp: MediaPlayer) {
|
||||
if (app.config.gaplessPlayback && mp == mCurrentMediaPlayer && mNextMediaPlayer != null) {
|
||||
isInitialized = false
|
||||
mCurrentMediaPlayer.reset()
|
||||
mCurrentMediaPlayer.release()
|
||||
mCurrentMediaPlayer = mNextMediaPlayer!!
|
||||
isInitialized = true
|
||||
mNextMediaPlayer = null
|
||||
callbacks.onTrackWentToNext()
|
||||
} else {
|
||||
callbacks.onTrackEnded()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setDataSourceImpl(player: MediaPlayer, trackUri: Uri, onPrepared: (success: Result<Boolean>) -> Unit) {
|
||||
player.reset()
|
||||
try {
|
||||
player.setDataSource(app, trackUri)
|
||||
player.setAudioAttributes(
|
||||
AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).setContentType(AudioAttributes.CONTENT_TYPE_MUSIC).build()
|
||||
)
|
||||
try {
|
||||
player.playbackParams = player.playbackParams.setSpeed(app.config.playbackSpeed)
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
|
||||
player.setOnPreparedListener {
|
||||
player.setOnPreparedListener(null)
|
||||
onPrepared(Result.success(true))
|
||||
}
|
||||
player.prepareAsync()
|
||||
} catch (e: Exception) {
|
||||
onPrepared(Result.failure(e))
|
||||
e.printStackTrace()
|
||||
}
|
||||
player.setOnCompletionListener(this)
|
||||
player.setOnErrorListener(this)
|
||||
}
|
||||
|
||||
private fun releaseNextPlayer() {
|
||||
mNextMediaPlayer?.release()
|
||||
mNextMediaPlayer = null
|
||||
}
|
||||
|
||||
private fun getAudioFocusRequest(): AudioFocusRequestCompat {
|
||||
if (audioFocusRequest == null) {
|
||||
val audioAttributes = AudioAttributesCompat.Builder()
|
||||
.setContentType(AudioAttributesCompat.CONTENT_TYPE_MUSIC)
|
||||
.build()
|
||||
audioFocusRequest = AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN)
|
||||
.setOnAudioFocusChangeListener(audioFocusListener)
|
||||
.setAudioAttributes(audioAttributes)
|
||||
.build()
|
||||
}
|
||||
return audioFocusRequest!!
|
||||
}
|
||||
|
||||
private fun requestFocus(): Boolean {
|
||||
return AudioManagerCompat.requestAudioFocus(audioManager!!, getAudioFocusRequest()) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
|
||||
}
|
||||
|
||||
private fun abandonFocus() {
|
||||
AudioManagerCompat.abandonAudioFocusRequest(audioManager!!, getAudioFocusRequest())
|
||||
}
|
||||
|
||||
fun setPlaybackSpeed(speed: Float) {
|
||||
mCurrentMediaPlayer.playbackParams = mCurrentMediaPlayer.playbackParams.setSpeed(speed)
|
||||
}
|
||||
|
||||
interface PlaybackCallbacks {
|
||||
fun onPrepared()
|
||||
|
||||
fun onTrackEnded()
|
||||
|
||||
fun onTrackWentToNext()
|
||||
|
||||
fun onPlayStateChanged()
|
||||
}
|
||||
|
||||
object Volume {
|
||||
const val DUCK = 0.2f
|
||||
const val NORMAL = 1.0f
|
||||
}
|
||||
}
|
|
@ -1,857 +0,0 @@
|
|||
package com.simplemobiletools.musicplayer.services
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.media.audiofx.Equalizer
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.Looper
|
||||
import android.support.v4.media.MediaMetadataCompat
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import android.support.v4.media.session.PlaybackStateCompat
|
||||
import androidx.media.session.MediaButtonReceiver
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.simplemobiletools.commons.extensions.*
|
||||
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
|
||||
import com.simplemobiletools.commons.helpers.isQPlus
|
||||
import com.simplemobiletools.commons.helpers.isSPlus
|
||||
import com.simplemobiletools.musicplayer.R
|
||||
import com.simplemobiletools.musicplayer.databases.SongsDatabase
|
||||
import com.simplemobiletools.musicplayer.extensions.*
|
||||
import com.simplemobiletools.musicplayer.helpers.*
|
||||
import com.simplemobiletools.musicplayer.helpers.PlaybackSetting.REPEAT_OFF
|
||||
import com.simplemobiletools.musicplayer.helpers.PlaybackSetting.REPEAT_PLAYLIST
|
||||
import com.simplemobiletools.musicplayer.helpers.PlaybackSetting.REPEAT_TRACK
|
||||
import com.simplemobiletools.musicplayer.helpers.PlaybackSetting.STOP_AFTER_CURRENT_TRACK
|
||||
import com.simplemobiletools.musicplayer.inlines.indexOfFirstOrNull
|
||||
import com.simplemobiletools.musicplayer.models.Events
|
||||
import com.simplemobiletools.musicplayer.models.QueueItem
|
||||
import com.simplemobiletools.musicplayer.models.Track
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import java.io.IOException
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class MusicService : Service(), MultiPlayer.PlaybackCallbacks {
|
||||
companion object {
|
||||
private const val PROGRESS_UPDATE_INTERVAL = 1000L
|
||||
private const val MAX_CLICK_DURATION = 700L
|
||||
private const val FAST_FORWARD_SKIP_MS = 10000
|
||||
|
||||
var mCurrTrack: Track? = null
|
||||
var mNextTrack: Track? = null
|
||||
var mTracks = ArrayList<Track>()
|
||||
var mPlayer: MultiPlayer? = null
|
||||
var mEqualizer: Equalizer? = null
|
||||
private var mCurrTrackCover: Bitmap? = null
|
||||
private var mHeadsetPlaceholder: Bitmap? = null
|
||||
private var mProgressHandler = Handler()
|
||||
private var mRetriedTrackCount = 0
|
||||
private var mPlaybackSpeed = 1f
|
||||
|
||||
private var mPlayOnPrepare = true
|
||||
private var mIsThirdPartyIntent = false
|
||||
private var mIntentUri: Uri? = null
|
||||
private var mMediaSession: MediaSessionCompat? = null
|
||||
var mIsServiceInitialized = false
|
||||
private var mSetProgressOnPrepare = 0
|
||||
private const val mMediaSessionActions =
|
||||
PlaybackStateCompat.ACTION_STOP or
|
||||
PlaybackStateCompat.ACTION_PAUSE or
|
||||
PlaybackStateCompat.ACTION_PLAY or
|
||||
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or
|
||||
PlaybackStateCompat.ACTION_SKIP_TO_NEXT or
|
||||
PlaybackStateCompat.ACTION_SEEK_TO or
|
||||
PlaybackStateCompat.ACTION_PLAY_PAUSE
|
||||
|
||||
fun isPlaying(): Boolean {
|
||||
return mPlayer != null && mPlayer!!.isPlaying()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
initMediaPlayerIfNeeded()
|
||||
|
||||
if (!isQPlus() && !hasPermission(getPermissionToRequest())) {
|
||||
EventBus.getDefault().post(Events.NoStoragePermission())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
destroyPlayer()
|
||||
mMediaSession?.isActive = false
|
||||
mMediaSession = null
|
||||
mEqualizer?.release()
|
||||
mEqualizer = null
|
||||
config.sleepInTS = 0L
|
||||
SongsDatabase.destroyInstance()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||
if (!isQPlus() && !hasPermission(getPermissionToRequest())) {
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
val action = intent.action
|
||||
when (action) {
|
||||
INIT -> handleInit(intent)
|
||||
INIT_PATH -> handleInitPath(intent)
|
||||
INIT_QUEUE -> handleInitQueue()
|
||||
PREVIOUS -> handlePrevious()
|
||||
PAUSE -> pauseTrack()
|
||||
PLAYPAUSE -> handlePlayPause()
|
||||
NEXT -> handleNext()
|
||||
PLAY_TRACK -> playTrack(intent)
|
||||
EDIT -> handleEdit(intent)
|
||||
FINISH -> handleFinish()
|
||||
FINISH_IF_NOT_PLAYING -> finishIfNotPlaying()
|
||||
DISMISS -> handleDismiss()
|
||||
REFRESH_LIST -> handleRefreshList()
|
||||
UPDATE_NEXT_TRACK -> broadcastNextTrackChange()
|
||||
SET_PROGRESS -> handleSetProgress(intent)
|
||||
SKIP_BACKWARD -> skip(false)
|
||||
SKIP_FORWARD -> skip(true)
|
||||
BROADCAST_STATUS -> broadcastPlayerStatus()
|
||||
SET_PLAYBACK_SPEED -> setPlaybackSpeed()
|
||||
UPDATE_QUEUE_SIZE -> updateQueueSize()
|
||||
UPDATE_GAPLESS_PLAYBACK -> updateGaplessPlayback()
|
||||
}
|
||||
|
||||
MediaButtonReceiver.handleIntent(mMediaSession!!, intent)
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
private fun initService(intent: Intent?) {
|
||||
mTracks.clear()
|
||||
mCurrTrack = null
|
||||
if (mIsThirdPartyIntent && mIntentUri != null) {
|
||||
val path = getRealPathFromURI(mIntentUri!!) ?: ""
|
||||
val track = RoomHelper(this).getTrackFromPath(path)
|
||||
if (track != null) {
|
||||
if (track.title.isEmpty()) {
|
||||
track.title = mIntentUri?.toString()?.getFilenameFromPath() ?: ""
|
||||
}
|
||||
|
||||
if (track.duration == 0) {
|
||||
try {
|
||||
val retriever = MediaMetadataRetriever()
|
||||
retriever.setDataSource(this, mIntentUri)
|
||||
val duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)!!.toInt() / 1000f
|
||||
track.duration = duration.roundToInt()
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
mTracks.add(track)
|
||||
}
|
||||
} else {
|
||||
mTracks = getQueuedTracks()
|
||||
|
||||
var wantedTrackId = intent?.getLongExtra(TRACK_ID, -1L) ?: -1L
|
||||
if (wantedTrackId == -1L) {
|
||||
val currentQueueItem = getNextQueueItem()
|
||||
wantedTrackId = currentQueueItem.trackId
|
||||
mSetProgressOnPrepare = currentQueueItem.lastPosition
|
||||
}
|
||||
|
||||
// use an old school loop to avoid ConcurrentModificationException
|
||||
for (i in 0 until mTracks.size) {
|
||||
val track = mTracks[i]
|
||||
if (track.mediaStoreId == wantedTrackId) {
|
||||
mCurrTrack = track
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
checkTrackOrder()
|
||||
}
|
||||
|
||||
initMediaPlayerIfNeeded()
|
||||
mIsServiceInitialized = true
|
||||
}
|
||||
|
||||
private fun handleInit(intent: Intent? = null) {
|
||||
mIsThirdPartyIntent = false
|
||||
ensureBackgroundThread {
|
||||
initService(intent)
|
||||
|
||||
val wantedTrackId = mCurrTrack?.mediaStoreId ?: -1L
|
||||
mPlayOnPrepare = true
|
||||
setTrack(wantedTrackId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleInitPath(intent: Intent) {
|
||||
mIsThirdPartyIntent = true
|
||||
if (mIntentUri != intent.data) {
|
||||
mIntentUri = intent.data
|
||||
initService(intent)
|
||||
initTracks()
|
||||
} else {
|
||||
updateUI()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleInitQueue() {
|
||||
ensureBackgroundThread {
|
||||
val unsortedTracks = getQueuedTracks()
|
||||
|
||||
mTracks.clear()
|
||||
val queuedItems = queueDAO.getAll()
|
||||
queuedItems.forEach { queueItem ->
|
||||
unsortedTracks.firstOrNull { it.mediaStoreId == queueItem.trackId }?.apply {
|
||||
mTracks.add(this)
|
||||
}
|
||||
}
|
||||
|
||||
checkTrackOrder()
|
||||
val currentQueueItem = queuedItems.firstOrNull { it.isCurrent } ?: queuedItems.firstOrNull()
|
||||
if (currentQueueItem != null) {
|
||||
mCurrTrack = getTrackWithId(currentQueueItem.trackId) ?: return@ensureBackgroundThread
|
||||
mPlayOnPrepare = false
|
||||
mSetProgressOnPrepare = currentQueueItem.lastPosition
|
||||
setTrack(mCurrTrack!!.mediaStoreId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handlePrevious() {
|
||||
mPlayOnPrepare = true
|
||||
playPreviousTrack()
|
||||
}
|
||||
|
||||
private fun handlePlayPause() {
|
||||
mPlayOnPrepare = true
|
||||
if (isPlaying()) {
|
||||
pauseTrack()
|
||||
} else {
|
||||
resumeTrack()
|
||||
}
|
||||
}
|
||||
|
||||
fun handleNext() {
|
||||
mPlayOnPrepare = true
|
||||
setupNextTrack()
|
||||
}
|
||||
|
||||
private fun handleEdit(intent: Intent) {
|
||||
mCurrTrack = intent.getSerializableExtra(EDITED_TRACK) as Track
|
||||
trackChanged()
|
||||
}
|
||||
|
||||
private fun finishIfNotPlaying() {
|
||||
if (!isPlaying()) {
|
||||
handleFinish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleFinish() {
|
||||
broadcastTrackProgress(0)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
fun handleDismiss() {
|
||||
if (isPlaying()) {
|
||||
pauseTrack(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRefreshList() {
|
||||
ensureBackgroundThread {
|
||||
mTracks = getQueuedTracks()
|
||||
checkTrackOrder()
|
||||
EventBus.getDefault().post(Events.QueueUpdated(mTracks))
|
||||
broadcastNextTrackChange()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSetProgress(intent: Intent) {
|
||||
if (mPlayer != null) {
|
||||
val progress = intent.getIntExtra(PROGRESS, getPosition()!! / 1000)
|
||||
updateProgress(progress)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupTrack() {
|
||||
if (mIsThirdPartyIntent) {
|
||||
initMediaPlayerIfNeeded()
|
||||
mPlayOnPrepare = true
|
||||
try {
|
||||
mPlayer!!.setDataSource(mIntentUri!!)
|
||||
val track = mTracks.first()
|
||||
mTracks.clear()
|
||||
mTracks.add(track)
|
||||
mCurrTrack = track
|
||||
updateUI()
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
} else {
|
||||
mPlayOnPrepare = false
|
||||
setupNextTrack()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initTracks() {
|
||||
if (mCurrTrack == null) {
|
||||
setupTrack()
|
||||
}
|
||||
updateUI()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun updateUI() {
|
||||
ensureBackgroundThread {
|
||||
if (mPlayer != null) {
|
||||
EventBus.getDefault().post(Events.QueueUpdated(mTracks))
|
||||
mCurrTrackCover = getAlbumImage().first
|
||||
trackChanged()
|
||||
|
||||
val secs = (getPosition() ?: 0) / 1000
|
||||
broadcastTrackProgress(secs)
|
||||
}
|
||||
trackStateChanged()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initMediaPlayerIfNeeded() {
|
||||
if (mPlayer == null) {
|
||||
mPlayer = MultiPlayer(app = applicationContext as Application, callbacks = this)
|
||||
setupEqualizer()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupEqualizer() {
|
||||
if (mPlayer == null) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val preset = config.equalizerPreset
|
||||
mEqualizer = Equalizer(0, mPlayer!!.getAudioSessionId())
|
||||
if (!mEqualizer!!.enabled) {
|
||||
mEqualizer!!.enabled = true
|
||||
}
|
||||
|
||||
if (preset != EQUALIZER_PRESET_CUSTOM) {
|
||||
mEqualizer!!.usePreset(preset.toShort())
|
||||
} else {
|
||||
val minValue = mEqualizer!!.bandLevelRange[0]
|
||||
val bandType = object : TypeToken<HashMap<Short, Int>>() {}.type
|
||||
val equalizerBands = Gson().fromJson<HashMap<Short, Int>>(config.equalizerBands, bandType) ?: HashMap()
|
||||
|
||||
for ((key, value) in equalizerBands) {
|
||||
val newValue = value + minValue
|
||||
if (mEqualizer!!.getBandLevel(key) != newValue.toShort()) {
|
||||
mEqualizer!!.setBandLevel(key, newValue.toShort())
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
// make sure tracks don't get duplicated in the queue, if they exist in multiple playlists
|
||||
private fun getQueuedTracks(): ArrayList<Track> {
|
||||
val tracks = ArrayList<Track>()
|
||||
val queueItems = queueDAO.getAll()
|
||||
val allTracks = audioHelper.getAllTracks()
|
||||
|
||||
// make sure we fetch the songs in the order they were displayed in
|
||||
val wantedIds = queueItems.map { it.trackId }
|
||||
val wantedTracks = ArrayList<Track>()
|
||||
for (wantedId in wantedIds) {
|
||||
val wantedTrack = allTracks.firstOrNull { it.mediaStoreId == wantedId }
|
||||
if (wantedTrack != null) {
|
||||
wantedTracks.add(wantedTrack)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
tracks.addAll(wantedTracks)
|
||||
return tracks.distinctBy { it.mediaStoreId }.toMutableList() as ArrayList<Track>
|
||||
}
|
||||
|
||||
private fun checkTrackOrder() {
|
||||
if (config.isShuffleEnabled) {
|
||||
mTracks.shuffle()
|
||||
|
||||
if (mCurrTrack != null) {
|
||||
mTracks.remove(mCurrTrack)
|
||||
mTracks.add(0, mCurrTrack!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTrackWithId(trackId: Long): Track? {
|
||||
return mTracks.firstOrNull { it.mediaStoreId == trackId }
|
||||
}
|
||||
|
||||
private fun getNextQueueItem(): QueueItem {
|
||||
return when (mTracks.size) {
|
||||
0 -> QueueItem.from(-1L)
|
||||
1 -> QueueItem.from(mTracks.first().mediaStoreId)
|
||||
else -> {
|
||||
val currentTrackIndex = mTracks.indexOfFirstOrNull { it.mediaStoreId == mCurrTrack?.mediaStoreId }
|
||||
if (currentTrackIndex != null) {
|
||||
val nextTrack = mTracks[(currentTrackIndex + 1) % mTracks.size]
|
||||
QueueItem.from(nextTrack.mediaStoreId)
|
||||
} else {
|
||||
queueDAO.getCurrent() ?: QueueItem.from(mTracks.first().mediaStoreId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isEndOfPlaylist(): Boolean {
|
||||
return when (mTracks.size) {
|
||||
0, 1 -> true
|
||||
else -> mCurrTrack?.mediaStoreId == mTracks.last().mediaStoreId
|
||||
}
|
||||
}
|
||||
|
||||
private fun playPreviousTrack() {
|
||||
if (mTracks.isEmpty()) {
|
||||
handleEmptyPlaylist()
|
||||
return
|
||||
}
|
||||
|
||||
initMediaPlayerIfNeeded()
|
||||
|
||||
// play the previous track if we are less than 5 secs into it, else restart
|
||||
val currentTrackIndex = mTracks.indexOfFirstOrNull { it.mediaStoreId == mCurrTrack?.mediaStoreId } ?: 0
|
||||
if (currentTrackIndex == 0 || getPosition()!! > 5000) {
|
||||
restartTrack()
|
||||
} else {
|
||||
val previousTrack = mTracks[currentTrackIndex - 1]
|
||||
setTrack(previousTrack.mediaStoreId)
|
||||
}
|
||||
}
|
||||
|
||||
fun pauseTrack(notify: Boolean = true) {
|
||||
initMediaPlayerIfNeeded()
|
||||
mPlayer!!.pause()
|
||||
trackStateChanged(false, notify = notify)
|
||||
updateMediaSessionState()
|
||||
saveTrackProgress()
|
||||
// do not call stopForeground on android 12 as it may cause a crash later
|
||||
if (!isSPlus()) {
|
||||
@Suppress("DEPRECATION")
|
||||
stopForeground(false)
|
||||
}
|
||||
}
|
||||
|
||||
fun resumeTrack() {
|
||||
if (mTracks.isEmpty()) {
|
||||
handleEmptyPlaylist()
|
||||
return
|
||||
}
|
||||
|
||||
initMediaPlayerIfNeeded()
|
||||
|
||||
if (mCurrTrack == null) {
|
||||
setupNextTrack()
|
||||
} else {
|
||||
mPlayer!!.start()
|
||||
}
|
||||
|
||||
setupEqualizer()
|
||||
trackStateChanged(true)
|
||||
setPlaybackSpeed()
|
||||
}
|
||||
|
||||
private fun setupNextTrack() {
|
||||
if (mIsThirdPartyIntent) {
|
||||
setupTrack()
|
||||
} else {
|
||||
ensureBackgroundThread {
|
||||
val queueItem = getNextQueueItem()
|
||||
mSetProgressOnPrepare = queueItem.lastPosition
|
||||
setTrack(queueItem.trackId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun restartTrack() {
|
||||
if (mCurrTrack != null) {
|
||||
setTrack(mCurrTrack!!.mediaStoreId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun playTrack(intent: Intent) {
|
||||
if (mIsThirdPartyIntent) {
|
||||
setupTrack()
|
||||
} else {
|
||||
mPlayOnPrepare = true
|
||||
val trackId = intent.getLongExtra(TRACK_ID, 0L)
|
||||
setTrack(trackId)
|
||||
broadcastTrackChange()
|
||||
}
|
||||
|
||||
mMediaSession?.isActive = true
|
||||
}
|
||||
|
||||
private fun setTrack(wantedTrackId: Long) {
|
||||
if (mTracks.isEmpty()) {
|
||||
handleEmptyPlaylist()
|
||||
return
|
||||
}
|
||||
|
||||
initMediaPlayerIfNeeded()
|
||||
mCurrTrack = getTrackWithId(wantedTrackId) ?: return
|
||||
|
||||
try {
|
||||
val trackUri = mCurrTrack!!.getUri()
|
||||
mPlayer!!.setDataSource(trackUri)
|
||||
trackChanged()
|
||||
} catch (e: IOException) {
|
||||
if (mCurrTrack != null) {
|
||||
val trackToDelete = mCurrTrack
|
||||
ensureBackgroundThread {
|
||||
audioHelper.deleteTrack(trackToDelete!!.mediaStoreId)
|
||||
}
|
||||
}
|
||||
|
||||
if (mRetriedTrackCount < 3) {
|
||||
mRetriedTrackCount++
|
||||
setupNextTrack()
|
||||
}
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun maybePrepareNext() {
|
||||
val isGapLess = config.gaplessPlayback
|
||||
val isPlayerInitialized = mPlayer != null && mPlayer!!.isInitialized
|
||||
if (!isGapLess || !isPlayerInitialized) {
|
||||
return
|
||||
}
|
||||
|
||||
val playbackSetting = config.playbackSetting
|
||||
if (playbackSetting == REPEAT_OFF && isEndOfPlaylist() || playbackSetting == STOP_AFTER_CURRENT_TRACK) {
|
||||
mPlayer!!.setNextDataSource(null)
|
||||
trackChanged()
|
||||
} else if (playbackSetting == REPEAT_TRACK) {
|
||||
prepareNext(nextTrack = mCurrTrack)
|
||||
} else {
|
||||
prepareNext()
|
||||
}
|
||||
}
|
||||
|
||||
private fun prepareNext(nextTrack: Track? = null) {
|
||||
mNextTrack = if (nextTrack != null) {
|
||||
nextTrack
|
||||
} else {
|
||||
val queueItem = getNextQueueItem()
|
||||
getTrackWithId(queueItem.trackId) ?: return
|
||||
}
|
||||
|
||||
try {
|
||||
val trackUri = mNextTrack!!.getUri()
|
||||
mPlayer!!.setNextDataSource(trackUri) {
|
||||
trackChanged()
|
||||
}
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleEmptyPlaylist() {
|
||||
mPlayer?.pause()
|
||||
mCurrTrack = null
|
||||
trackChanged()
|
||||
trackStateChanged(false)
|
||||
|
||||
if (!mIsServiceInitialized) {
|
||||
handleInit()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onTrackEnded() {
|
||||
if (!config.autoplay) {
|
||||
return
|
||||
}
|
||||
|
||||
val playbackSetting = config.playbackSetting
|
||||
|
||||
mPlayOnPrepare = when (playbackSetting) {
|
||||
REPEAT_OFF -> !isEndOfPlaylist()
|
||||
REPEAT_PLAYLIST, REPEAT_TRACK -> true
|
||||
STOP_AFTER_CURRENT_TRACK -> false
|
||||
}
|
||||
|
||||
when (playbackSetting) {
|
||||
REPEAT_OFF -> {
|
||||
if (isEndOfPlaylist()) {
|
||||
broadcastTrackProgress(0)
|
||||
setupNextTrack()
|
||||
} else {
|
||||
setupNextTrack()
|
||||
}
|
||||
}
|
||||
|
||||
REPEAT_PLAYLIST -> setupNextTrack()
|
||||
REPEAT_TRACK -> restartTrack()
|
||||
STOP_AFTER_CURRENT_TRACK -> {
|
||||
broadcastTrackProgress(0)
|
||||
restartTrack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTrackWentToNext() {
|
||||
mCurrTrack = mNextTrack
|
||||
maybePrepareNext()
|
||||
trackStateChanged()
|
||||
}
|
||||
|
||||
override fun onPrepared() {
|
||||
mRetriedTrackCount = 0
|
||||
if (mPlayOnPrepare) {
|
||||
mPlayer!!.start()
|
||||
if (mIsThirdPartyIntent) {
|
||||
trackChanged()
|
||||
}
|
||||
}
|
||||
if (mSetProgressOnPrepare > 0) {
|
||||
mPlayer!!.seek(mSetProgressOnPrepare)
|
||||
broadcastTrackProgress(mSetProgressOnPrepare / 1000)
|
||||
mSetProgressOnPrepare = 0
|
||||
}
|
||||
|
||||
maybePrepareNext()
|
||||
trackStateChanged()
|
||||
}
|
||||
|
||||
override fun onPlayStateChanged() {
|
||||
trackStateChanged()
|
||||
}
|
||||
|
||||
private fun trackChanged() {
|
||||
broadcastTrackChange()
|
||||
updateMediaSession()
|
||||
updateMediaSessionState()
|
||||
}
|
||||
|
||||
private fun updateMediaSession() {
|
||||
val (albumImage, isPlaceholder) = getAlbumImage()
|
||||
mCurrTrackCover = albumImage
|
||||
var lockScreenImage = if (isPlaceholder) albumImage else null
|
||||
if (lockScreenImage == null || lockScreenImage.isRecycled) {
|
||||
try {
|
||||
lockScreenImage = resources.getColoredBitmap(R.drawable.ic_headset, getProperTextColor())
|
||||
} catch (ignored: OutOfMemoryError) {
|
||||
}
|
||||
}
|
||||
|
||||
val metadata = MediaMetadataCompat.Builder()
|
||||
.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, lockScreenImage)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, mCurrTrack?.album ?: "")
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, mCurrTrack?.artist ?: "")
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, mCurrTrack?.title ?: "")
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mCurrTrack?.mediaStoreId?.toString())
|
||||
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, (mCurrTrack?.duration?.toLong() ?: 0L) * 1000)
|
||||
.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, mTracks.indexOf(mCurrTrack).toLong() + 1)
|
||||
.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, mTracks.size.toLong())
|
||||
.build()
|
||||
|
||||
mMediaSession?.setMetadata(metadata)
|
||||
}
|
||||
|
||||
private fun updateMediaSessionState() {
|
||||
val builder = PlaybackStateCompat.Builder()
|
||||
val playbackState = if (isPlaying()) {
|
||||
PlaybackStateCompat.STATE_PLAYING
|
||||
} else {
|
||||
PlaybackStateCompat.STATE_PAUSED
|
||||
}
|
||||
|
||||
val dismissAction = PlaybackStateCompat.CustomAction.Builder(
|
||||
DISMISS,
|
||||
getString(R.string.dismiss),
|
||||
R.drawable.ic_cross_vector
|
||||
).build()
|
||||
|
||||
builder
|
||||
.setActions(mMediaSessionActions)
|
||||
.setState(playbackState, getPosition()?.toLong() ?: 0L, mPlaybackSpeed)
|
||||
.addCustomAction(dismissAction)
|
||||
try {
|
||||
mMediaSession?.setPlaybackState(builder.build())
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateQueueSize() {
|
||||
updateMediaSession()
|
||||
}
|
||||
|
||||
private fun updateGaplessPlayback() {
|
||||
maybePrepareNext()
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
private fun setPlaybackSpeed() {
|
||||
if (mPlayer != null) {
|
||||
mPlaybackSpeed = config.playbackSpeed
|
||||
if (isPlaying()) {
|
||||
try {
|
||||
mPlayer!!.setPlaybackSpeed(mPlaybackSpeed)
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun broadcastTrackChange() {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
broadcastUpdateWidgetState()
|
||||
EventBus.getDefault().post(Events.TrackChanged(mCurrTrack))
|
||||
broadcastNextTrackChange()
|
||||
}
|
||||
saveTrackProgress()
|
||||
}
|
||||
|
||||
private fun broadcastTrackStateChange(isPlaying: Boolean) {
|
||||
broadcastUpdateWidgetState()
|
||||
EventBus.getDefault().post(Events.TrackStateChanged(isPlaying))
|
||||
}
|
||||
|
||||
private fun broadcastNextTrackChange() {
|
||||
setPlaybackSpeed()
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
var currentTrackIndex: Int? = null
|
||||
for (i in 0 until mTracks.size) {
|
||||
val track = mTracks[i]
|
||||
if (track.mediaStoreId == mCurrTrack?.mediaStoreId) {
|
||||
currentTrackIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (currentTrackIndex != null) {
|
||||
val nextTrack = mTracks[(currentTrackIndex + 1) % mTracks.size]
|
||||
EventBus.getDefault().post(Events.NextTrackChanged(nextTrack))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun broadcastTrackProgress(progress: Int) {
|
||||
EventBus.getDefault().post(Events.ProgressUpdated(progress))
|
||||
updateMediaSessionState()
|
||||
}
|
||||
|
||||
private fun getPosition(): Int? {
|
||||
return mPlayer?.position()
|
||||
}
|
||||
|
||||
// do not just return the album cover, but also a boolean to indicate if it a real cover, or just the placeholder
|
||||
@SuppressLint("NewApi")
|
||||
private fun getAlbumImage(): Pair<Bitmap, Boolean> {
|
||||
val coverArt = loadTrackCoverArt(mCurrTrack)
|
||||
if (coverArt != null) {
|
||||
return Pair(coverArt, true)
|
||||
}
|
||||
|
||||
if (mHeadsetPlaceholder == null) {
|
||||
mHeadsetPlaceholder = resources.getColoredBitmap(R.drawable.ic_headset, getProperTextColor())
|
||||
}
|
||||
|
||||
return Pair(mHeadsetPlaceholder!!, false)
|
||||
}
|
||||
|
||||
private fun destroyPlayer() {
|
||||
if (!mIsThirdPartyIntent) {
|
||||
ensureBackgroundThread {
|
||||
try {
|
||||
saveTrackProgress()
|
||||
mTracks.forEachIndexed { index, track ->
|
||||
queueDAO.setOrder(track.mediaStoreId, index)
|
||||
}
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
|
||||
mCurrTrack = null
|
||||
}
|
||||
} else {
|
||||
mCurrTrack = null
|
||||
}
|
||||
|
||||
mPlayer?.release()
|
||||
mPlayer = null
|
||||
|
||||
trackStateChanged(isPlaying = false, notify = false)
|
||||
trackChanged()
|
||||
|
||||
stopSelf()
|
||||
mIsThirdPartyIntent = false
|
||||
mIsServiceInitialized = false
|
||||
}
|
||||
|
||||
fun updateProgress(progress: Int) {
|
||||
mPlayer!!.seek(progress * 1000)
|
||||
saveTrackProgress()
|
||||
resumeTrack()
|
||||
}
|
||||
|
||||
private fun trackStateChanged(isPlaying: Boolean = isPlaying(), notify: Boolean = true) {
|
||||
handleProgressHandler(isPlaying)
|
||||
broadcastTrackStateChange(isPlaying)
|
||||
}
|
||||
|
||||
private fun handleProgressHandler(isPlaying: Boolean) {
|
||||
if (isPlaying) {
|
||||
mProgressHandler.post(object : Runnable {
|
||||
override fun run() {
|
||||
if (isPlaying()) {
|
||||
val secs = getPosition()!! / 1000
|
||||
broadcastTrackProgress(secs)
|
||||
}
|
||||
mProgressHandler.removeCallbacksAndMessages(null)
|
||||
mProgressHandler.postDelayed(this, (PROGRESS_UPDATE_INTERVAL / mPlaybackSpeed).toLong())
|
||||
}
|
||||
})
|
||||
} else {
|
||||
mProgressHandler.removeCallbacksAndMessages(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun skip(forward: Boolean) {
|
||||
val curr = getPosition() ?: return
|
||||
val newProgress = if (forward) curr + FAST_FORWARD_SKIP_MS else curr - FAST_FORWARD_SKIP_MS
|
||||
mPlayer!!.seek(newProgress)
|
||||
resumeTrack()
|
||||
}
|
||||
|
||||
// used at updating the widget at create or resize
|
||||
private fun broadcastPlayerStatus() {
|
||||
broadcastTrackStateChange(isPlaying())
|
||||
broadcastTrackChange()
|
||||
broadcastNextTrackChange()
|
||||
broadcastTrackProgress((getPosition() ?: 0) / 1000)
|
||||
}
|
||||
|
||||
private fun saveTrackProgress() {
|
||||
if (mCurrTrack != null) {
|
||||
ensureBackgroundThread {
|
||||
val trackId = mCurrTrack?.mediaStoreId ?: return@ensureBackgroundThread
|
||||
val position = getPosition()
|
||||
queueDAO.apply {
|
||||
resetCurrent()
|
||||
if (position == null || position == 0) {
|
||||
saveCurrentTrack(trackId)
|
||||
} else {
|
||||
saveCurrentTrackProgress(trackId, position)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue