commit
bc39d3a462
20 changed files with 315 additions and 172 deletions
|
@ -14,8 +14,8 @@ android {
|
|||
vectorDrawables.useSupportLibrary = true
|
||||
|
||||
applicationId "code.name.monkey.retromusic"
|
||||
versionCode 10591
|
||||
versionName '6.0.0-beta'
|
||||
versionCode 10592
|
||||
versionName '6.0.1-beta'
|
||||
|
||||
buildConfigField("String", "GOOGLE_PLAY_LICENSING_KEY", "\"${getProperty(getProperties('../public.properties'), 'GOOGLE_PLAY_LICENSE_KEY')}\"")
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ android {
|
|||
}
|
||||
}
|
||||
lint {
|
||||
disable 'MissingTranslation', 'ImpliedQuantity'
|
||||
warning 'MissingTranslation', 'ImpliedQuantity', 'Instantiatable'
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
|
@ -105,7 +105,7 @@ dependencies {
|
|||
implementation "androidx.navigation:navigation-fragment-ktx:$navigation_version"
|
||||
implementation "androidx.navigation:navigation-ui-ktx:$navigation_version"
|
||||
|
||||
def room_version = '2.4.2'
|
||||
def room_version = '2.5.0-alpha02'
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
implementation "androidx.room:room-ktx:$room_version"
|
||||
kapt "androidx.room:room-compiler:$room_version"
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28" />
|
||||
android:maxSdkVersion="29" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.BROADCAST_STICKY" />
|
||||
|
@ -34,12 +34,12 @@
|
|||
android:configChanges="locale|layoutDirection"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:restoreAnyVersion="true"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.RetroMusic.FollowSystem"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
tools:targetApi="m">
|
||||
<activity
|
||||
android:name=".activities.MainActivity"
|
||||
|
@ -194,7 +194,7 @@
|
|||
</provider>
|
||||
|
||||
<receiver
|
||||
android:name="androidx.media.session.MediaButtonReceiver"
|
||||
android:name=".service.MediaButtonIntentReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
|
@ -305,9 +305,6 @@
|
|||
<intent-filter>
|
||||
<action android:name="android.media.browse.MediaBrowserService" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<meta-data
|
||||
|
|
|
@ -62,6 +62,17 @@
|
|||
</head>
|
||||
|
||||
<body>
|
||||
<div>
|
||||
<h5>June 13, 2022</h5>
|
||||
<h2>v6.0.1<span class="tag"><i>Beta</i></span></h2>
|
||||
<h3>Fixed</h3>
|
||||
<ul>
|
||||
<li>Fixed ChromeCast crash</li>
|
||||
<li>Fixed Slider crashes</li>
|
||||
<li>Fixed storage related crashes on Android 10</li>
|
||||
<li>Fixed CrossFade not working Fade Audio is not working</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h5>June 7, 2022</h5>
|
||||
<h2>v6.0.0<span class="tag"><i>Beta</i></span></h2>
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -191,7 +191,7 @@ abstract class AbsMusicServiceActivity : AbsBaseActivity(), IMusicServiceEventLi
|
|||
|
||||
override fun getPermissionsToRequest(): Array<String> {
|
||||
return mutableListOf(Manifest.permission.READ_EXTERNAL_STORAGE).apply {
|
||||
if (!VersionUtils.hasQ()) {
|
||||
if (!VersionUtils.hasR()) {
|
||||
add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
}
|
||||
}.toTypedArray()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
} else {
|
||||
val embeddedLyrics = LyricUtil.getEmbeddedSyncedLyrics(song.data)
|
||||
withContext(Dispatchers.Main) {
|
||||
if (embeddedLyrics != null) {
|
||||
binding.lyricsView.loadLrc(embeddedLyrics)
|
||||
} else {
|
||||
binding.lyricsView.reset()
|
||||
}
|
||||
binding.lyricsView.setLabel(context?.getString(R.string.no_lyrics_found))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<KeyEvent>(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<PowerManager>()
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,12 +770,11 @@ class MusicService : MediaBrowserServiceCompat(),
|
|||
|
||||
@Synchronized
|
||||
fun play() {
|
||||
playbackManager.play(onNotInitialized = { playSongAt(getPosition()) }) {
|
||||
playbackManager.play { playSongAt(getPosition()) }
|
||||
if (notHandledMetaChangedForCurrentTrack) {
|
||||
handleChangeInternal(META_CHANGED)
|
||||
notHandledMetaChangedForCurrentTrack = false
|
||||
}
|
||||
}
|
||||
notifyChange(PLAY_STATE_CHANGED)
|
||||
}
|
||||
|
||||
|
@ -803,7 +795,9 @@ class MusicService : MediaBrowserServiceCompat(),
|
|||
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)
|
||||
serviceScope.launch(IO) {
|
||||
savePosition()
|
||||
savePositionInTrack()
|
||||
serviceScope.launch(IO) {
|
||||
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,12 +1204,16 @@ class MusicService : MediaBrowserServiceCompat(),
|
|||
openQueue(playlistSongs, 0, true)
|
||||
}
|
||||
} else {
|
||||
runOnUiThread {
|
||||
showToast(R.string.playlist_is_empty, Toast.LENGTH_LONG)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
runOnUiThread {
|
||||
showToast(R.string.playlist_is_empty, Toast.LENGTH_LONG)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun prepareNext() {
|
||||
prepareNextImpl()
|
||||
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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<MusicService> = WeakReference(musicService)
|
||||
|
||||
override fun handleMessage(msg: Message) {
|
||||
val service: MusicService? = service.get()
|
||||
if (msg.what == SAVE_QUEUES) {
|
||||
service?.saveQueuesImpl()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue