June 7, 2022
v6.0.0Beta
diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/DriveModeActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/DriveModeActivity.kt
index 1fb04f469..9c6178aaf 100644
--- a/app/src/main/java/code/name/monkey/retromusic/activities/DriveModeActivity.kt
+++ b/app/src/main/java/code/name/monkey/retromusic/activities/DriveModeActivity.kt
@@ -14,12 +14,10 @@
*/
package code.name.monkey.retromusic.activities
-import android.animation.ObjectAnimator
import android.content.Intent
import android.graphics.Color
import android.graphics.PorterDuff
import android.os.Bundle
-import android.view.animation.LinearInterpolator
import androidx.lifecycle.lifecycleScope
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.activities.base.AbsMusicServiceActivity
@@ -27,7 +25,6 @@ import code.name.monkey.retromusic.databinding.ActivityDriveModeBinding
import code.name.monkey.retromusic.db.toSongEntity
import code.name.monkey.retromusic.extensions.accentColor
import code.name.monkey.retromusic.extensions.drawAboveSystemBars
-import code.name.monkey.retromusic.fragments.base.AbsPlayerControlsFragment
import code.name.monkey.retromusic.glide.BlurTransformation
import code.name.monkey.retromusic.glide.GlideApp
import code.name.monkey.retromusic.glide.RetroGlideExtension
@@ -246,12 +243,10 @@ class DriveModeActivity : AbsMusicServiceActivity(), Callback {
}
override fun onUpdateProgressViews(progress: Int, total: Int) {
- binding.progressSlider.valueTo = total.toFloat()
-
- val animator = ObjectAnimator.ofFloat(binding.progressSlider, "value", progress.toFloat())
- animator.duration = AbsPlayerControlsFragment.SLIDER_ANIMATION_TIME
- animator.interpolator = LinearInterpolator()
- animator.start()
+ binding.progressSlider.run {
+ valueTo = total.toFloat()
+ value = progress.toFloat().coerceIn(valueFrom, valueTo)
+ }
binding.songTotalTime.text = MusicUtil.getReadableDurationString(total.toLong())
binding.songCurrentProgress.text = MusicUtil.getReadableDurationString(progress.toLong())
diff --git a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsMusicServiceActivity.kt b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsMusicServiceActivity.kt
index 59c6320d8..07937c7b5 100644
--- a/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsMusicServiceActivity.kt
+++ b/app/src/main/java/code/name/monkey/retromusic/activities/base/AbsMusicServiceActivity.kt
@@ -191,7 +191,7 @@ abstract class AbsMusicServiceActivity : AbsBaseActivity(), IMusicServiceEventLi
override fun getPermissionsToRequest(): Array
{
return mutableListOf(Manifest.permission.READ_EXTERNAL_STORAGE).apply {
- if (!VersionUtils.hasQ()) {
+ if (!VersionUtils.hasR()) {
add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
}.toTypedArray()
diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsPlayerControlsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsPlayerControlsFragment.kt
index 0a12de582..78659cd50 100644
--- a/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsPlayerControlsFragment.kt
+++ b/app/src/main/java/code/name/monkey/retromusic/fragments/base/AbsPlayerControlsFragment.kt
@@ -82,9 +82,8 @@ abstract class AbsPlayerControlsFragment(@LayoutRes layout: Int) : AbsMusicServi
if (seekBar == null) {
progressSlider?.valueTo = total.toFloat()
- if (progress > total) return
- progressSlider?.value = progress.toFloat()
-
+ progressSlider?.value =
+ progress.toFloat().coerceIn(progressSlider?.valueFrom, progressSlider?.valueTo)
} else {
seekBar?.max = total
diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/other/MiniPlayerFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/other/MiniPlayerFragment.kt
index fe3180e64..552773b91 100644
--- a/app/src/main/java/code/name/monkey/retromusic/fragments/other/MiniPlayerFragment.kt
+++ b/app/src/main/java/code/name/monkey/retromusic/fragments/other/MiniPlayerFragment.kt
@@ -14,7 +14,6 @@
*/
package code.name.monkey.retromusic.fragments.other
-import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle
@@ -23,7 +22,6 @@ import android.text.style.ForegroundColorSpan
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
-import android.view.animation.DecelerateInterpolator
import androidx.core.text.toSpannable
import androidx.core.view.isVisible
import code.name.monkey.retromusic.R
@@ -139,10 +137,7 @@ open class MiniPlayerFragment : AbsMusicServiceFragment(R.layout.fragment_mini_p
override fun onUpdateProgressViews(progress: Int, total: Int) {
binding.progressBar.max = total
- val animator = ObjectAnimator.ofInt(binding.progressBar, "progress", progress)
- animator.duration = 1000
- animator.interpolator = DecelerateInterpolator()
- animator.start()
+ binding.progressBar.progress = progress
}
override fun onResume() {
diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/player/PlayerAlbumCoverFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/player/PlayerAlbumCoverFragment.kt
index d0db68330..3770da872 100644
--- a/app/src/main/java/code/name/monkey/retromusic/fragments/player/PlayerAlbumCoverFragment.kt
+++ b/app/src/main/java/code/name/monkey/retromusic/fragments/player/PlayerAlbumCoverFragment.kt
@@ -52,7 +52,6 @@ import code.name.monkey.retromusic.util.color.MediaNotificationProcessor
import code.name.monkey.retromusic.util.logD
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
class PlayerAlbumCoverFragment : AbsMusicServiceFragment(R.layout.fragment_player_album_cover),
ViewPager.OnPageChangeListener, MusicProgressViewUpdateHelper.Callback,
@@ -86,22 +85,18 @@ class PlayerAlbumCoverFragment : AbsMusicServiceFragment(R.layout.fragment_playe
}
private fun updateLyrics() {
- binding.lyricsView.setLabel(context?.getString(R.string.no_lyrics_found))
val song = MusicPlayerRemote.currentSong
lifecycleScope.launch(Dispatchers.IO) {
val lrcFile = LyricUtil.getSyncedLyricsFile(song)
if (lrcFile != null) {
- withContext(Dispatchers.Main) {
- binding.lyricsView.loadLrc(lrcFile)
- }
+ binding.lyricsView.loadLrc(lrcFile)
} else {
val embeddedLyrics = LyricUtil.getEmbeddedSyncedLyrics(song.data)
- withContext(Dispatchers.Main) {
- if (embeddedLyrics != null) {
- binding.lyricsView.loadLrc(embeddedLyrics)
- } else {
- binding.lyricsView.reset()
- }
+ if (embeddedLyrics != null) {
+ binding.lyricsView.loadLrc(embeddedLyrics)
+ } else {
+ binding.lyricsView.reset()
+ binding.lyricsView.setLabel(context?.getString(R.string.no_lyrics_found))
}
}
}
diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/player/circle/CirclePlayerFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/player/circle/CirclePlayerFragment.kt
index 4186bad55..c98c4fdd6 100644
--- a/app/src/main/java/code/name/monkey/retromusic/fragments/player/circle/CirclePlayerFragment.kt
+++ b/app/src/main/java/code/name/monkey/retromusic/fragments/player/circle/CirclePlayerFragment.kt
@@ -35,7 +35,6 @@ import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.databinding.FragmentCirclePlayerBinding
import code.name.monkey.retromusic.extensions.*
import code.name.monkey.retromusic.fragments.MusicSeekSkipTouchListener
-import code.name.monkey.retromusic.fragments.base.AbsPlayerControlsFragment
import code.name.monkey.retromusic.fragments.base.AbsPlayerFragment
import code.name.monkey.retromusic.fragments.base.goToAlbum
import code.name.monkey.retromusic.fragments.base.goToArtist
@@ -315,16 +314,10 @@ class CirclePlayerFragment : AbsPlayerFragment(R.layout.fragment_circle_player),
val progressSlider = binding.progressSlider
progressSlider.valueTo = total.toFloat()
- if (isSeeking) {
- progressSlider.value = progress.toFloat()
- } else {
- progressAnimator =
- ObjectAnimator.ofFloat(progressSlider, "value", progress.toFloat()).apply {
- duration = AbsPlayerControlsFragment.SLIDER_ANIMATION_TIME
- interpolator = LinearInterpolator()
- start()
- }
- }
+ progressSlider.valueTo = total.toFloat()
+
+ progressSlider.value =
+ progress.toFloat().coerceIn(progressSlider.valueFrom, progressSlider.valueTo)
binding.songTotalTime.text = MusicUtil.getReadableDurationString(total.toLong())
binding.songCurrentProgress.text = MusicUtil.getReadableDurationString(progress.toLong())
diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/player/gradient/GradientPlayerFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/player/gradient/GradientPlayerFragment.kt
index 048751906..e79bd784d 100644
--- a/app/src/main/java/code/name/monkey/retromusic/fragments/player/gradient/GradientPlayerFragment.kt
+++ b/app/src/main/java/code/name/monkey/retromusic/fragments/player/gradient/GradientPlayerFragment.kt
@@ -22,7 +22,6 @@ import android.graphics.PorterDuff
import android.graphics.drawable.AnimatedVectorDrawable
import android.os.Bundle
import android.view.View
-import android.view.animation.LinearInterpolator
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.PopupMenu
import androidx.appcompat.widget.Toolbar
@@ -41,7 +40,6 @@ import code.name.monkey.retromusic.adapter.song.PlayingQueueAdapter
import code.name.monkey.retromusic.databinding.FragmentGradientPlayerBinding
import code.name.monkey.retromusic.extensions.*
import code.name.monkey.retromusic.fragments.MusicSeekSkipTouchListener
-import code.name.monkey.retromusic.fragments.base.AbsPlayerControlsFragment
import code.name.monkey.retromusic.fragments.base.AbsPlayerFragment
import code.name.monkey.retromusic.fragments.base.goToAlbum
import code.name.monkey.retromusic.fragments.base.goToArtist
@@ -573,16 +571,9 @@ class GradientPlayerFragment : AbsPlayerFragment(R.layout.fragment_gradient_play
val progressSlider = binding.playbackControlsFragment.progressSlider
progressSlider.valueTo = total.toFloat()
- if (isSeeking) {
- progressSlider.value = progress.toFloat()
- } else {
- progressAnimator =
- ObjectAnimator.ofFloat(progressSlider, "value", progress.toFloat()).apply {
- duration = AbsPlayerControlsFragment.SLIDER_ANIMATION_TIME
- interpolator = LinearInterpolator()
- start()
- }
- }
+ progressSlider.value =
+ progress.toFloat().coerceIn(progressSlider.valueFrom, progressSlider.valueTo)
+
binding.playbackControlsFragment.songTotalTime.text =
MusicUtil.getReadableDurationString(total.toLong())
binding.playbackControlsFragment.songCurrentProgress.text =
diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistDetailsFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistDetailsFragment.kt
index 5dc7aad09..1a71944c1 100644
--- a/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistDetailsFragment.kt
+++ b/app/src/main/java/code/name/monkey/retromusic/fragments/playlists/PlaylistDetailsFragment.kt
@@ -24,6 +24,7 @@ import code.name.monkey.retromusic.interfaces.ICabCallback
import code.name.monkey.retromusic.interfaces.ICabHolder
import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.util.RetroColorUtil
+import code.name.monkey.retromusic.util.ThemedFastScroller
import com.afollestad.materialcab.attached.AttachedCab
import com.afollestad.materialcab.attached.destroy
import com.afollestad.materialcab.attached.isActive
@@ -109,6 +110,7 @@ class PlaylistDetailsFragment : AbsMainActivityFragment(R.layout.fragment_playli
binding.recyclerView.apply {
layoutManager = LinearLayoutManager(requireContext())
binding.recyclerView.adapter = wrappedAdapter
+ ThemedFastScroller.create(this)
}
playlistSongAdapter.registerAdapterDataObserver(object :
RecyclerView.AdapterDataObserver() {
diff --git a/app/src/main/java/code/name/monkey/retromusic/helper/MusicProgressViewUpdateHelper.kt b/app/src/main/java/code/name/monkey/retromusic/helper/MusicProgressViewUpdateHelper.kt
index 3562ef914..37deb038e 100644
--- a/app/src/main/java/code/name/monkey/retromusic/helper/MusicProgressViewUpdateHelper.kt
+++ b/app/src/main/java/code/name/monkey/retromusic/helper/MusicProgressViewUpdateHelper.kt
@@ -24,9 +24,10 @@ class MusicProgressViewUpdateHelper : Handler {
private var callback: Callback? = null
private var intervalPlaying: Int = 0
private var intervalPaused: Int = 0
+ private var firstUpdate = true
fun start() {
- queueNextRefresh(1)
+ queueNextRefresh(refreshProgressViews().toLong())
}
fun stop() {
@@ -59,10 +60,11 @@ class MusicProgressViewUpdateHelper : Handler {
private fun refreshProgressViews(): Int {
val progressMillis = MusicPlayerRemote.songProgressMillis
val totalMillis = MusicPlayerRemote.songDurationMillis
- if (totalMillis > 0)
+ if (totalMillis > 0) {
+ firstUpdate = false
callback?.onUpdateProgressViews(progressMillis, totalMillis)
-
- if (!MusicPlayerRemote.isPlaying) {
+ }
+ if (!MusicPlayerRemote.isPlaying && !firstUpdate) {
return intervalPaused
}
@@ -84,7 +86,7 @@ class MusicProgressViewUpdateHelper : Handler {
companion object {
private const val CMD_REFRESH_PROGRESS_VIEWS = 1
private const val MIN_INTERVAL = 20
- private const val UPDATE_INTERVAL_PLAYING = 1000
+ private const val UPDATE_INTERVAL_PLAYING = 500
private const val UPDATE_INTERVAL_PAUSED = 500
}
}
diff --git a/app/src/main/java/code/name/monkey/retromusic/service/AudioFader.kt b/app/src/main/java/code/name/monkey/retromusic/service/AudioFader.kt
index 009b0c40d..3548a8c27 100644
--- a/app/src/main/java/code/name/monkey/retromusic/service/AudioFader.kt
+++ b/app/src/main/java/code/name/monkey/retromusic/service/AudioFader.kt
@@ -10,16 +10,16 @@ import code.name.monkey.retromusic.util.PreferenceUtil
class AudioFader {
companion object {
- inline fun createFadeAnimator(
+ fun createFadeAnimator(
fadeInMp: MediaPlayer,
fadeOutMp: MediaPlayer,
- crossinline endAction: (animator: Animator) -> Unit, /* Code to run when Animator Ends*/
+ endAction: (animator: Animator) -> Unit, /* Code to run when Animator Ends*/
): Animator? {
val duration = PreferenceUtil.crossFadeDuration * 1000
if (duration == 0) {
return null
}
- return ValueAnimator.ofFloat(1f, 0f).apply {
+ return ValueAnimator.ofFloat(0f, 1f).apply {
this.duration = duration.toLong()
addUpdateListener { animation: ValueAnimator ->
fadeInMp.setVolume(
@@ -34,15 +34,14 @@ class AudioFader {
}
}
- @JvmStatic
fun startFadeAnimator(
playback: Playback,
fadeIn: Boolean, /* fadeIn -> true fadeOut -> false*/
- callback: Runnable, /* Code to run when Animator Ends*/
+ callback: Runnable? = null, /* Code to run when Animator Ends*/
) {
val duration = PreferenceUtil.audioFadeDuration.toLong()
if (duration == 0L) {
- callback.run()
+ callback?.run()
return
}
val startValue = if (fadeIn) 0f else 1.0f
@@ -50,12 +49,10 @@ class AudioFader {
val animator = ValueAnimator.ofFloat(startValue, endValue)
animator.duration = duration
animator.addUpdateListener { animation: ValueAnimator ->
- playback.setVolume(
- animation.animatedValue as Float
- )
+ playback.setVolume(animation.animatedValue as Float)
}
animator.doOnEnd {
- callback.run()
+ callback?.run()
}
animator.start()
}
diff --git a/app/src/main/java/code/name/monkey/retromusic/service/CastPlayer.kt b/app/src/main/java/code/name/monkey/retromusic/service/CastPlayer.kt
index 2a67051d6..514612e33 100644
--- a/app/src/main/java/code/name/monkey/retromusic/service/CastPlayer.kt
+++ b/app/src/main/java/code/name/monkey/retromusic/service/CastPlayer.kt
@@ -10,8 +10,7 @@ import com.google.android.gms.cast.MediaStatus
import com.google.android.gms.cast.framework.CastSession
import com.google.android.gms.cast.framework.media.RemoteMediaClient
-class CastPlayer(castSession: CastSession) : Playback,
- RemoteMediaClient.Callback() {
+class CastPlayer(castSession: CastSession) : Playback, RemoteMediaClient.Callback() {
override val isInitialized: Boolean = true
diff --git a/app/src/main/java/code/name/monkey/retromusic/service/CrossFadePlayer.kt b/app/src/main/java/code/name/monkey/retromusic/service/CrossFadePlayer.kt
index da27e6b36..7ce084f7c 100644
--- a/app/src/main/java/code/name/monkey/retromusic/service/CrossFadePlayer.kt
+++ b/app/src/main/java/code/name/monkey/retromusic/service/CrossFadePlayer.kt
@@ -37,7 +37,7 @@ class CrossFadePlayer(context: Context) : LocalPlayback(context) {
private var crossFadeAnimator: Animator? = null
override var callbacks: PlaybackCallbacks? = null
private var crossFadeDuration = PreferenceUtil.crossFadeDuration
- private var isCrossFading = false
+ var isCrossFading = false
init {
player1.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK)
@@ -303,7 +303,7 @@ class CrossFadePlayer(context: Context) : LocalPlayback(context) {
private fun switchPlayer() {
getNextPlayer()?.start()
- crossFade(getCurrentPlayer()!!, getNextPlayer()!!)
+ crossFade(getNextPlayer()!!, getCurrentPlayer()!!)
currentPlayer =
if (currentPlayer == CurrentPlayer.PLAYER_ONE || currentPlayer == CurrentPlayer.NOT_SET) {
CurrentPlayer.PLAYER_TWO
diff --git a/app/src/main/java/code/name/monkey/retromusic/service/MediaButtonIntentReceiver.kt b/app/src/main/java/code/name/monkey/retromusic/service/MediaButtonIntentReceiver.kt
new file mode 100644
index 000000000..d0e36e924
--- /dev/null
+++ b/app/src/main/java/code/name/monkey/retromusic/service/MediaButtonIntentReceiver.kt
@@ -0,0 +1,201 @@
+/*
+ * Copyright (c) 2019 Hemanth Savarala.
+ *
+ * Licensed under the GNU General Public License v3
+ *
+ * This is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by
+ * the Free Software Foundation either version 3 of the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ */
+
+
+package code.name.monkey.retromusic.service
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.os.Handler
+import android.os.Message
+import android.os.PowerManager
+import android.os.PowerManager.WakeLock
+import android.util.Log
+import android.view.KeyEvent
+import androidx.core.content.ContextCompat
+import androidx.core.content.getSystemService
+import androidx.media.session.MediaButtonReceiver
+import code.name.monkey.retromusic.BuildConfig
+import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_PAUSE
+import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_PLAY
+import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_REWIND
+import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_SKIP
+import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_STOP
+import code.name.monkey.retromusic.service.MusicService.Companion.ACTION_TOGGLE_PAUSE
+
+
+/**
+ * Used to control headset playback.
+ * Single press: pause/resume
+ * Double press: actionNext track
+ * Triple press: previous track
+ */
+class MediaButtonIntentReceiver : MediaButtonReceiver() {
+
+ override fun onReceive(context: Context, intent: Intent) {
+ if (DEBUG) Log.v(TAG, "Received intent: $intent")
+ if (handleIntent(context, intent) && isOrderedBroadcast) {
+ abortBroadcast()
+ }
+ }
+
+ companion object {
+ val TAG: String = MediaButtonIntentReceiver::class.java.simpleName
+ private val DEBUG = BuildConfig.DEBUG
+ private const val MSG_HEADSET_DOUBLE_CLICK_TIMEOUT = 2
+
+ private const val DOUBLE_CLICK = 400
+
+ private var wakeLock: WakeLock? = null
+ private var mClickCounter = 0
+ private var mLastClickTime: Long = 0
+
+ @SuppressLint("HandlerLeak") // false alarm, handler is already static
+ private val mHandler = object : Handler() {
+
+ override fun handleMessage(msg: Message) {
+ when (msg.what) {
+ MSG_HEADSET_DOUBLE_CLICK_TIMEOUT -> {
+ val clickCount = msg.arg1
+
+ if (DEBUG) Log.v(TAG, "Handling headset click, count = $clickCount")
+ val command = when (clickCount) {
+ 1 -> ACTION_TOGGLE_PAUSE
+ 2 -> ACTION_SKIP
+ 3 -> ACTION_REWIND
+ else -> null
+ }
+
+ if (command != null) {
+ val context = msg.obj as Context
+ startService(context, command)
+ }
+ }
+ }
+ releaseWakeLockIfHandlerIdle()
+ }
+ }
+
+ fun handleIntent(context: Context, intent: Intent): Boolean {
+ println("Intent Action: ${intent.action}")
+ val intentAction = intent.action
+ if (Intent.ACTION_MEDIA_BUTTON == intentAction) {
+ val event = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT)
+ ?: return false
+
+ val keycode = event.keyCode
+ val action = event.action
+ val eventTime = if (event.eventTime != 0L)
+ event.eventTime
+ else
+ System.currentTimeMillis()
+
+ var command: String? = null
+ when (keycode) {
+ KeyEvent.KEYCODE_MEDIA_STOP -> command = ACTION_STOP
+ KeyEvent.KEYCODE_HEADSETHOOK, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> command =
+ ACTION_TOGGLE_PAUSE
+ KeyEvent.KEYCODE_MEDIA_NEXT -> command = ACTION_SKIP
+ KeyEvent.KEYCODE_MEDIA_PREVIOUS -> command = ACTION_REWIND
+ KeyEvent.KEYCODE_MEDIA_PAUSE -> command = ACTION_PAUSE
+ KeyEvent.KEYCODE_MEDIA_PLAY -> command = ACTION_PLAY
+ }
+ if (command != null) {
+ if (action == KeyEvent.ACTION_DOWN) {
+ if (event.repeatCount == 0) {
+ // Only consider the first event in a sequence, not the repeat events,
+ // so that we don't trigger in cases where the first event went to
+ // a different app (e.g. when the user ends a phone call by
+ // long pressing the headset button)
+
+ // The service may or may not be running, but we need to send it
+ // a command.
+ if (keycode == KeyEvent.KEYCODE_HEADSETHOOK || keycode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
+ if (eventTime - mLastClickTime >= DOUBLE_CLICK) {
+ mClickCounter = 0
+ }
+
+ mClickCounter++
+ if (DEBUG) Log.v(TAG, "Got headset click, count = $mClickCounter")
+ mHandler.removeMessages(MSG_HEADSET_DOUBLE_CLICK_TIMEOUT)
+
+ val msg = mHandler.obtainMessage(
+ MSG_HEADSET_DOUBLE_CLICK_TIMEOUT, mClickCounter, 0, context
+ )
+
+ val delay = (if (mClickCounter < 3) DOUBLE_CLICK else 0).toLong()
+ if (mClickCounter >= 3) {
+ mClickCounter = 0
+ }
+ mLastClickTime = eventTime
+ acquireWakeLockAndSendMessage(context, msg, delay)
+ } else {
+ startService(context, command)
+ }
+ return true
+ }
+ }
+ }
+ }
+ return false
+ }
+
+ private fun startService(context: Context, command: String?) {
+ val intent = Intent(context, MusicService::class.java)
+ intent.action = command
+ try {
+ // IMPORTANT NOTE: (kind of a hack)
+ // on Android O and above the following crashes when the app is not running
+ // there is no good way to check whether the app is running so we catch the exception
+ // we do not always want to use startForegroundService() because then one gets an ANR
+ // if no notification is displayed via startForeground()
+ // according to Play analytics this happens a lot, I suppose for example if command = PAUSE
+ context.startService(intent)
+ } catch (ignored: IllegalStateException) {
+ ContextCompat.startForegroundService(context, intent)
+ }
+ }
+
+ private fun acquireWakeLockAndSendMessage(context: Context, msg: Message, delay: Long) {
+ if (wakeLock == null) {
+ val appContext = context.applicationContext
+ val pm = appContext.getSystemService()
+ wakeLock = pm?.newWakeLock(
+ PowerManager.PARTIAL_WAKE_LOCK,
+ "RetroMusicApp:Wakelock headset button"
+ )
+ wakeLock!!.setReferenceCounted(false)
+ }
+ if (DEBUG) Log.v(TAG, "Acquiring wake lock and sending " + msg.what)
+ // Make sure we don't indefinitely hold the wake lock under any circumstances
+ wakeLock!!.acquire(10000)
+
+ mHandler.sendMessageDelayed(msg, delay)
+ }
+
+ private fun releaseWakeLockIfHandlerIdle() {
+ if (mHandler.hasMessages(MSG_HEADSET_DOUBLE_CLICK_TIMEOUT)) {
+ if (DEBUG) Log.v(TAG, "Handler still has messages pending, not releasing wake lock")
+ return
+ }
+
+ if (wakeLock != null) {
+ if (DEBUG) Log.v(TAG, "Releasing wake lock")
+ wakeLock!!.release()
+ wakeLock = null
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/code/name/monkey/retromusic/service/MusicService.kt b/app/src/main/java/code/name/monkey/retromusic/service/MusicService.kt
index 16a550136..d1c274cc9 100644
--- a/app/src/main/java/code/name/monkey/retromusic/service/MusicService.kt
+++ b/app/src/main/java/code/name/monkey/retromusic/service/MusicService.kt
@@ -15,6 +15,7 @@ package code.name.monkey.retromusic.service
import android.annotation.SuppressLint
import android.app.NotificationManager
+import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothDevice.EXTRA_DEVICE
@@ -39,7 +40,6 @@ import android.widget.Toast
import androidx.core.content.edit
import androidx.core.content.getSystemService
import androidx.media.MediaBrowserServiceCompat
-import androidx.media.session.MediaButtonReceiver.handleIntent
import androidx.preference.PreferenceManager
import code.name.monkey.appthemehelper.util.VersionUtils
import code.name.monkey.retromusic.*
@@ -94,6 +94,7 @@ import kotlinx.coroutines.Dispatchers.Main
import org.koin.java.KoinJavaComponent.get
import java.util.*
+
/**
* @author Karim Abou Zeid (kabouzeid), Andrew Neal. Modified by Prathamesh More
*/
@@ -203,8 +204,7 @@ class MusicService : MediaBrowserServiceCompat(),
}
}
}
- private var queueSaveHandler: QueueSaveHandler? = null
- private var queueSaveHandlerThread: HandlerThread? = null
+
private var queuesRestored = false
var repeatMode = 0
@@ -277,12 +277,6 @@ class MusicService : MediaBrowserServiceCompat(),
playbackManager.setCallbacks(this)
setupMediaSession()
- // queue saving needs to run on a separate thread so that it doesn't block the playback handler
- // events
- queueSaveHandlerThread =
- HandlerThread("QueueSaveHandler", Process.THREAD_PRIORITY_BACKGROUND)
- queueSaveHandlerThread?.start()
- queueSaveHandler = QueueSaveHandler(this, queueSaveHandlerThread!!.looper)
uiThreadHandler = Handler(Looper.getMainLooper())
registerReceiver(widgetIntentReceiver, IntentFilter(APP_WIDGET_UPDATE))
registerReceiver(updateFavoriteReceiver, IntentFilter(FAVORITE_STATE_CHANGED))
@@ -291,7 +285,7 @@ class MusicService : MediaBrowserServiceCompat(),
notificationManager = getSystemService()
initNotification()
mediaStoreObserver = MediaStoreObserver(this, playerHandler!!)
- throttledSeekHandler = ThrottledSeekHandler(this, playerHandler!!)
+ throttledSeekHandler = ThrottledSeekHandler(this, Handler(mainLooper))
contentResolver.registerContentObserver(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
true,
mediaStoreObserver)
@@ -656,7 +650,6 @@ class MusicService : MediaBrowserServiceCompat(),
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent != null && intent.action != null) {
- handleIntent(mediaSession, intent)
serviceScope.launch {
restoreQueuesAndPositionIfNecessary()
when (intent.action) {
@@ -761,11 +754,11 @@ class MusicService : MediaBrowserServiceCompat(),
this.position = position
openCurrent { success ->
completion(success)
- notifyChange(META_CHANGED)
- notHandledMetaChangedForCurrentTrack = false
if (success) {
prepareNextImpl()
}
+ notifyChange(META_CHANGED)
+ notHandledMetaChangedForCurrentTrack = false
}
}
@@ -777,11 +770,10 @@ class MusicService : MediaBrowserServiceCompat(),
@Synchronized
fun play() {
- playbackManager.play(onNotInitialized = { playSongAt(getPosition()) }) {
- if (notHandledMetaChangedForCurrentTrack) {
- handleChangeInternal(META_CHANGED)
- notHandledMetaChangedForCurrentTrack = false
- }
+ playbackManager.play { playSongAt(getPosition()) }
+ if (notHandledMetaChangedForCurrentTrack) {
+ handleChangeInternal(META_CHANGED)
+ notHandledMetaChangedForCurrentTrack = false
}
notifyChange(PLAY_STATE_CHANGED)
}
@@ -798,12 +790,14 @@ class MusicService : MediaBrowserServiceCompat(),
// Every chromecast method needs to run on main thread or you are greeted with IllegalStateException
// So it will use Main dispatcher
// And by using Default dispatcher for local playback we are reduce the burden of main thread
- serviceScope.launch(if(playbackManager.isLocalPlayback) Default else Main) {
+ serviceScope.launch(if (playbackManager.isLocalPlayback) Default else Main) {
openTrackAndPrepareNextAt(position) { success ->
if (success) {
play()
} else {
- showToast(resources.getString(R.string.unplayable_file))
+ runOnUiThread {
+ showToast(R.string.unplayable_file)
+ }
}
}
}
@@ -953,17 +947,6 @@ class MusicService : MediaBrowserServiceCompat(),
}
}
- fun saveQueuesImpl() {
- MusicPlaybackQueueStore.getInstance(this).saveQueues(playingQueue, originalPlayingQueue)
- }
-
- fun saveState() {
- saveQueues()
- savePosition()
- savePositionInTrack()
- storage.saveSong(currentSong)
- }
-
@Synchronized
fun seek(millis: Int): Int {
return try {
@@ -1104,9 +1087,9 @@ class MusicService : MediaBrowserServiceCompat(),
// We must call updateMediaSessionPlaybackState after the load of album art is completed
// if we are loading it or it won't be updated in the notification
updateMediaSessionMetaData(::updateMediaSessionPlaybackState)
+ savePosition()
+ savePositionInTrack()
serviceScope.launch(IO) {
- savePosition()
- savePositionInTrack()
val currentSong = currentSong
HistoryStore.getInstance(this@MusicService).addSongId(currentSong.id)
if (songPlayCountHelper.shouldBumpPlayCount()) {
@@ -1114,13 +1097,14 @@ class MusicService : MediaBrowserServiceCompat(),
.bumpPlayCount(songPlayCountHelper.song.id)
}
songPlayCountHelper.notifySongChanged(currentSong)
+ storage.saveSong(currentSong)
}
}
QUEUE_CHANGED -> {
mediaSession?.setQueueTitle(getString(R.string.now_playing_queue))
mediaSession?.setQueue(playingQueue.toMediaSessionQueue())
updateMediaSessionMetaData(::updateMediaSessionPlaybackState) // because playing queue size might have changed
- saveState()
+ saveQueues()
if (playingQueue.size > 0) {
prepareNext()
} else {
@@ -1198,6 +1182,8 @@ class MusicService : MediaBrowserServiceCompat(),
seek(progress)
if (wasPlaying) {
play()
+ } else {
+ pause()
}
}
}
@@ -1218,10 +1204,14 @@ class MusicService : MediaBrowserServiceCompat(),
openQueue(playlistSongs, 0, true)
}
} else {
- showToast(R.string.playlist_is_empty, Toast.LENGTH_LONG)
+ runOnUiThread {
+ showToast(R.string.playlist_is_empty, Toast.LENGTH_LONG)
+ }
}
} else {
- showToast(R.string.playlist_is_empty, Toast.LENGTH_LONG)
+ runOnUiThread {
+ showToast(R.string.playlist_is_empty, Toast.LENGTH_LONG)
+ }
}
}
@@ -1247,8 +1237,6 @@ class MusicService : MediaBrowserServiceCompat(),
private fun releaseResources() {
playerHandler?.removeCallbacksAndMessages(null)
musicPlayerHandlerThread?.quitSafely()
- queueSaveHandler?.removeCallbacksAndMessages(null)
- queueSaveHandlerThread?.quitSafely()
playbackManager.release()
mediaSession?.release()
}
@@ -1275,8 +1263,10 @@ class MusicService : MediaBrowserServiceCompat(),
}
private fun saveQueues() {
- queueSaveHandler?.removeMessages(SAVE_QUEUES)
- queueSaveHandler?.sendEmptyMessage(SAVE_QUEUES)
+ serviceScope.launch(IO) {
+ MusicPlaybackQueueStore.getInstance(this@MusicService)
+ .saveQueues(playingQueue, originalPlayingQueue)
+ }
}
private fun sendChangeInternal(what: String) {
@@ -1314,13 +1304,25 @@ class MusicService : MediaBrowserServiceCompat(),
}
private fun setupMediaSession() {
+ val mediaButtonReceiverComponentName = ComponentName(applicationContext,
+ MediaButtonIntentReceiver::class.java)
+
+ val mediaButtonIntent = Intent(Intent.ACTION_MEDIA_BUTTON)
+ mediaButtonIntent.component = mediaButtonReceiverComponentName
+ val mediaButtonReceiverPendingIntent = PendingIntent.getBroadcast(
+ applicationContext, 0, mediaButtonIntent,
+ if (VersionUtils.hasMarshmallow()) PendingIntent.FLAG_IMMUTABLE else 0
+ )
mediaSession = MediaSessionCompat(
this,
- "RetroMusicPlayer"
+ BuildConfig.APPLICATION_ID,
+ mediaButtonReceiverComponentName,
+ mediaButtonReceiverPendingIntent
)
val mediaSessionCallback = MediaSessionCallback(this)
mediaSession?.setCallback(mediaSessionCallback)
mediaSession?.isActive = true
+ mediaSession?.setMediaButtonReceiver(mediaButtonReceiverPendingIntent)
}
inner class MusicBinder : Binder() {
@@ -1368,7 +1370,6 @@ class MusicService : MediaBrowserServiceCompat(),
const val REPEAT_MODE_NONE = 0
const val REPEAT_MODE_ALL = 1
const val REPEAT_MODE_THIS = 2
- const val SAVE_QUEUES = 0
private const val MEDIA_SESSION_ACTIONS = (PlaybackStateCompat.ACTION_PLAY
or PlaybackStateCompat.ACTION_PAUSE
or PlaybackStateCompat.ACTION_PLAY_PAUSE
diff --git a/app/src/main/java/code/name/monkey/retromusic/service/PlaybackManager.kt b/app/src/main/java/code/name/monkey/retromusic/service/PlaybackManager.kt
index 9186bd534..83fa7302b 100644
--- a/app/src/main/java/code/name/monkey/retromusic/service/PlaybackManager.kt
+++ b/app/src/main/java/code/name/monkey/retromusic/service/PlaybackManager.kt
@@ -16,7 +16,7 @@ class PlaybackManager(val context: Context) {
var playback: Playback? = null
private var playbackLocation = PlaybackLocation.LOCAL
- val isLocalPlayback get() = playbackLocation== PlaybackLocation.LOCAL
+ val isLocalPlayback get() = playbackLocation == PlaybackLocation.LOCAL
val audioSessionId: Int
get() = if (playback != null) {
@@ -47,16 +47,19 @@ class PlaybackManager(val context: Context) {
playback?.callbacks = callbacks
}
- fun play(onNotInitialized: () -> Unit = {}, onPlay: () -> Unit = {}) {
+ fun play(onNotInitialized: () -> Unit) {
if (playback != null && !playback!!.isPlaying) {
if (!playback!!.isInitialized) {
onNotInitialized()
} else {
openAudioEffectSession()
if (playbackLocation == PlaybackLocation.LOCAL) {
- AudioFader.startFadeAnimator(playback!!, true) {
- // Code when Animator Ends
- onPlay()
+ if (playback is CrossFadePlayer) {
+ if (!(playback as CrossFadePlayer).isCrossFading) {
+ AudioFader.startFadeAnimator(playback!!, true)
+ }
+ } else {
+ AudioFader.startFadeAnimator(playback!!, true)
}
}
if (shouldSetSpeed) {
@@ -168,14 +171,11 @@ class PlaybackManager(val context: Context) {
playback: Playback,
onChange: (wasPlaying: Boolean, progress: Int) -> Unit,
) {
- val oldPlayback = playback
- val wasPlaying: Boolean = oldPlayback.isPlaying
- val progress: Int = oldPlayback.position()
-
+ val oldPlayback = this.playback
+ val wasPlaying: Boolean = oldPlayback?.isPlaying == true
+ val progress: Int = oldPlayback?.position() ?: 0
this.playback = playback
-
- oldPlayback.stop()
-
+ oldPlayback?.stop()
onChange(wasPlaying, progress)
}
diff --git a/app/src/main/java/code/name/monkey/retromusic/service/QueueSaveHandler.kt b/app/src/main/java/code/name/monkey/retromusic/service/QueueSaveHandler.kt
deleted file mode 100644
index 9fd1b91ad..000000000
--- a/app/src/main/java/code/name/monkey/retromusic/service/QueueSaveHandler.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (c) 2019 Hemanth Savarala.
- *
- * Licensed under the GNU General Public License v3
- *
- * This is free software: you can redistribute it and/or modify it under
- * the terms of the GNU General Public License as published by
- * the Free Software Foundation either version 3 of the License, or (at your option) any later version.
- *
- * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
- * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
- * See the GNU General Public License for more details.
- */
-
-package code.name.monkey.retromusic.service
-
-import android.os.Handler
-import android.os.Looper
-import android.os.Message
-import code.name.monkey.retromusic.service.MusicService.Companion.SAVE_QUEUES
-import java.lang.ref.WeakReference
-
-internal class QueueSaveHandler(
- musicService: MusicService,
- looper: Looper
-) : Handler(looper) {
- private val service: WeakReference = WeakReference(musicService)
-
- override fun handleMessage(msg: Message) {
- val service: MusicService? = service.get()
- if (msg.what == SAVE_QUEUES) {
- service?.saveQueuesImpl()
- }
- }
-}
diff --git a/build.gradle b/build.gradle
index d1d3d8338..431658a9a 100644
--- a/build.gradle
+++ b/build.gradle
@@ -2,7 +2,7 @@
buildscript {
ext {
- kotlin_version = '1.6.21'
+ kotlin_version = '1.7.0'
navigation_version = '2.5.0-rc01'
mdc_version = '1.7.0-alpha02'
preference_version = '1.2.0'