Fix up and repackage

This commit is contained in:
JFronny 2022-05-14 15:47:55 +02:00
parent 4df292bddf
commit cde7fd6565
No known key found for this signature in database
GPG key ID: E76429612C2929F4
510 changed files with 2660 additions and 3312 deletions

View file

@ -0,0 +1,66 @@
package io.github.muntashirakon.music.service
import android.animation.Animator
import android.animation.ValueAnimator
import android.media.MediaPlayer
import androidx.core.animation.doOnEnd
import io.github.muntashirakon.music.service.playback.Playback
import io.github.muntashirakon.music.util.PreferenceUtil
class AudioFader {
companion object {
@JvmStatic
inline fun createFadeAnimator(
fadeIn: Boolean /* fadeIn -> true fadeOut -> false*/,
mediaPlayer: MediaPlayer,
crossinline endAction: (animator: Animator) -> Unit /* Code to run when Animator Ends*/
): Animator? {
val duration = PreferenceUtil.crossFadeDuration * 1000
if (duration == 0) {
return null
}
val startValue = if (fadeIn) 0f else 1.0f
val endValue = if (fadeIn) 1.0f else 0f
return ValueAnimator.ofFloat(startValue, endValue).apply {
this.duration = duration.toLong()
addUpdateListener { animation: ValueAnimator ->
mediaPlayer.setVolume(
animation.animatedValue as Float, animation.animatedValue as Float
)
}
doOnEnd {
endAction(it)
// Set end values
mediaPlayer.setVolume(endValue, endValue)
}
}
}
@JvmStatic
fun startFadeAnimator(
playback: Playback,
fadeIn: Boolean /* fadeIn -> true fadeOut -> false*/,
callback: Runnable /* Code to run when Animator Ends*/
) {
val duration = PreferenceUtil.audioFadeDuration.toLong()
if (duration == 0L) {
callback.run()
return
}
val startValue = if (fadeIn) 0f else 1.0f
val endValue = if (fadeIn) 1.0f else 0f
val animator = ValueAnimator.ofFloat(startValue, endValue)
animator.duration = duration
animator.addUpdateListener { animation: ValueAnimator ->
playback.setVolume(
animation.animatedValue as Float
)
}
animator.doOnEnd {
callback.run()
}
animator.start()
}
}
}

View file

@ -0,0 +1,368 @@
package io.github.muntashirakon.music.service
import android.animation.Animator
import android.content.Context
import android.content.Intent
import android.media.AudioAttributes
import android.media.AudioManager
import android.media.MediaPlayer
import android.media.PlaybackParams
import android.media.audiofx.AudioEffect
import android.os.PowerManager
import android.util.Log
import androidx.core.net.toUri
import code.name.monkey.appthemehelper.util.VersionUtils.hasMarshmallow
import io.github.muntashirakon.music.R
import io.github.muntashirakon.music.extensions.showToast
import io.github.muntashirakon.music.helper.MusicPlayerRemote
import io.github.muntashirakon.music.service.AudioFader.Companion.createFadeAnimator
import io.github.muntashirakon.music.service.playback.Playback
import io.github.muntashirakon.music.service.playback.Playback.PlaybackCallbacks
import io.github.muntashirakon.music.util.MusicUtil
import io.github.muntashirakon.music.util.PreferenceUtil
import io.github.muntashirakon.music.util.PreferenceUtil.playbackPitch
import io.github.muntashirakon.music.util.PreferenceUtil.playbackSpeed
import kotlinx.coroutines.*
/** @author Prathamesh M */
/*
* To make Crossfade work we need two MediaPlayer's
* Basically, we switch back and forth between those two mp's
* e.g. When song is about to end (Reaches Crossfade duration) we let current mediaplayer
* play but with decreasing volume and start the player with the next song with increasing volume
* and vice versa for upcoming song and so on.
*/
class CrossFadePlayer(val context: Context) : Playback, MediaPlayer.OnCompletionListener,
MediaPlayer.OnErrorListener {
private var currentPlayer: CurrentPlayer = CurrentPlayer.NOT_SET
private var player1 = MediaPlayer()
private var player2 = MediaPlayer()
private var durationListener = DurationListener()
private var mIsInitialized = false
private var hasDataSource: Boolean = false /* Whether first player has DataSource */
private var fadeInAnimator: Animator? = null
private var fadeOutAnimator: Animator? = null
private var callbacks: PlaybackCallbacks? = null
private var crossFadeDuration = PreferenceUtil.crossFadeDuration
init {
player1.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK)
player2.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK)
currentPlayer = CurrentPlayer.PLAYER_ONE
}
override fun start(): Boolean {
durationListener.start()
return try {
getCurrentPlayer()?.start()
true
} catch (e: IllegalStateException) {
e.printStackTrace()
false
}
}
override fun release() {
getCurrentPlayer()?.release()
getNextPlayer()?.release()
durationListener.stop()
}
override fun setCallbacks(callbacks: PlaybackCallbacks) {
this.callbacks = callbacks
}
override fun stop() {
getCurrentPlayer()?.reset()
mIsInitialized = false
}
override fun pause(): Boolean {
durationListener.stop()
cancelFade()
getCurrentPlayer()?.let {
if (it.isPlaying) {
it.pause()
}
}
getNextPlayer()?.let {
if (it.isPlaying) {
it.pause()
}
}
return true
}
override fun seek(whereto: Int): Int {
cancelFade()
getNextPlayer()?.stop()
return try {
getCurrentPlayer()?.seekTo(whereto)
whereto
} catch (e: java.lang.IllegalStateException) {
e.printStackTrace()
-1
}
}
override fun setVolume(vol: Float): Boolean {
cancelFade()
return try {
getCurrentPlayer()?.setVolume(vol, vol)
true
} catch (e: IllegalStateException) {
e.printStackTrace()
false
}
}
override val isInitialized: Boolean
get() = mIsInitialized
override val isPlaying: Boolean
get() = mIsInitialized && getCurrentPlayer()?.isPlaying == true
override fun setDataSource(path: String, force: Boolean): Boolean {
cancelFade()
if (force) hasDataSource = false
mIsInitialized = false
/* We've already set DataSource if initialized is true in setNextDataSource */
if (!hasDataSource) {
getCurrentPlayer()?.let { mIsInitialized = setDataSourceImpl(it, path) }
hasDataSource = true
} else {
mIsInitialized = true
}
return mIsInitialized
}
override fun setNextDataSource(path: String?) {}
/**
* @param player The {@link MediaPlayer} to use
* @param path The path of the file, or the http/rtsp URL of the stream you want to play
* @return True if the <code>player</code> has been prepared and is ready to play, false otherwise
*/
private fun setDataSourceImpl(
player: MediaPlayer,
path: String,
): Boolean {
player.reset()
player.setOnPreparedListener(null)
try {
if (path.startsWith("content://")) {
player.setDataSource(context, path.toUri())
} else {
player.setDataSource(path)
}
player.setAudioAttributes(
AudioAttributes.Builder().setLegacyStreamType(AudioManager.STREAM_MUSIC).build()
)
player.prepare()
player.setPlaybackSpeedPitch(playbackSpeed, playbackPitch)
} catch (e: Exception) {
e.printStackTrace()
return false
}
player.setOnCompletionListener(this)
player.setOnErrorListener(this)
val intent = Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION)
intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId)
intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.packageName)
intent.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC)
context.sendBroadcast(intent)
return true
}
override fun setAudioSessionId(sessionId: Int): Boolean {
return try {
getCurrentPlayer()?.audioSessionId = sessionId
true
} catch (e: IllegalArgumentException) {
e.printStackTrace()
false
} catch (e: IllegalStateException) {
e.printStackTrace()
false
}
}
override val audioSessionId: Int
get() = getCurrentPlayer()?.audioSessionId!!
/**
* Gets the duration of the file.
*
* @return The duration in milliseconds
*/
override fun duration(): Int {
return if (!mIsInitialized) {
-1
} else try {
getCurrentPlayer()?.duration!!
} catch (e: IllegalStateException) {
e.printStackTrace()
-1
}
}
/**
* Gets the current position in audio.
* @return The position in milliseconds
*/
override fun position(): Int {
return if (!mIsInitialized) {
-1
} else try {
getCurrentPlayer()?.currentPosition!!
} catch (e: IllegalStateException) {
e.printStackTrace()
-1
}
}
override fun onCompletion(mp: MediaPlayer?) {
if (mp == getCurrentPlayer()) {
callbacks?.onTrackEnded()
}
}
private fun getCurrentPlayer(): MediaPlayer? {
return when (currentPlayer) {
CurrentPlayer.PLAYER_ONE -> {
player1
}
CurrentPlayer.PLAYER_TWO -> {
player2
}
CurrentPlayer.NOT_SET -> {
null
}
}
}
private fun getNextPlayer(): MediaPlayer? {
return when (currentPlayer) {
CurrentPlayer.PLAYER_ONE -> {
player2
}
CurrentPlayer.PLAYER_TWO -> {
player1
}
CurrentPlayer.NOT_SET -> {
null
}
}
}
private fun fadeIn(mediaPlayer: MediaPlayer) {
fadeInAnimator = createFadeAnimator(true, mediaPlayer) {
fadeInAnimator = null
durationListener.start()
}
fadeInAnimator?.start()
}
private fun fadeOut(mediaPlayer: MediaPlayer) {
fadeOutAnimator = createFadeAnimator(false, mediaPlayer) {
fadeOutAnimator = null
mediaPlayer.stop()
}
fadeOutAnimator?.start()
}
private fun cancelFade() {
fadeInAnimator = null
fadeOutAnimator = null
}
override fun onError(mp: MediaPlayer?, what: Int, extra: Int): Boolean {
mIsInitialized = false
mp?.release()
player1 = MediaPlayer()
player2 = MediaPlayer()
mIsInitialized = true
mp?.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK)
context.showToast(R.string.unplayable_file)
Log.e(TAG, what.toString() + extra)
return false
}
enum class CurrentPlayer {
PLAYER_ONE,
PLAYER_TWO,
NOT_SET
}
inner class DurationListener : CoroutineScope by crossFadeScope() {
private var job: Job? = null
fun start() {
job?.cancel()
job = launch {
while (true) {
delay(250)
onDurationUpdated(position(), duration())
}
}
}
fun stop() {
job?.cancel()
}
}
fun onDurationUpdated(progress: Int, total: Int) {
if (total > 0 && (total - progress).div(1000) == crossFadeDuration) {
getNextPlayer()?.let { player ->
val nextSong = MusicPlayerRemote.nextSong
if (nextSong != null) {
setDataSourceImpl(player, MusicUtil.getSongFileUri(nextSong.id).toString())
// Switch to other player / Crossfade only if next song exists
switchPlayer()
}
}
}
}
private fun switchPlayer() {
getNextPlayer()?.start()
getCurrentPlayer()?.let { fadeOut(it) }
getNextPlayer()?.let { fadeIn(it) }
currentPlayer =
if (currentPlayer == CurrentPlayer.PLAYER_ONE || currentPlayer == CurrentPlayer.NOT_SET) {
CurrentPlayer.PLAYER_TWO
} else {
CurrentPlayer.PLAYER_ONE
}
callbacks?.onTrackEndedWithCrossfade()
}
override fun setCrossFadeDuration(duration: Int) {
crossFadeDuration = duration
}
override fun setPlaybackSpeedPitch(speed: Float, pitch: Float) {
getCurrentPlayer()?.setPlaybackSpeedPitch(speed, pitch)
}
private fun MediaPlayer.setPlaybackSpeedPitch(speed: Float, pitch: Float) {
if (hasMarshmallow()) {
val wasPlaying: Boolean = isPlaying
playbackParams = PlaybackParams().setSpeed(speed).setPitch(pitch)
if (!wasPlaying) {
if (isPlaying) pause()
}
}
}
companion object {
val TAG: String = CrossFadePlayer::class.java.simpleName
}
}
internal fun crossFadeScope(): CoroutineScope = CoroutineScope(Job() + Dispatchers.Main)

View file

@ -0,0 +1,213 @@
/*
* 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 io.github.muntashirakon.music.service
import android.content.Context
import android.os.Bundle
import android.provider.MediaStore
import android.support.v4.media.session.MediaSessionCompat
import io.github.muntashirakon.music.auto.AutoMediaIDHelper
import io.github.muntashirakon.music.helper.MusicPlayerRemote
import io.github.muntashirakon.music.helper.MusicPlayerRemote.cycleRepeatMode
import io.github.muntashirakon.music.helper.ShuffleHelper.makeShuffleList
import io.github.muntashirakon.music.model.Album
import io.github.muntashirakon.music.model.Artist
import io.github.muntashirakon.music.model.Playlist
import io.github.muntashirakon.music.model.Song
import io.github.muntashirakon.music.repository.*
import io.github.muntashirakon.music.service.MusicService.Companion.CYCLE_REPEAT
import io.github.muntashirakon.music.service.MusicService.Companion.TOGGLE_FAVORITE
import io.github.muntashirakon.music.service.MusicService.Companion.TOGGLE_SHUFFLE
import io.github.muntashirakon.music.util.MusicUtil
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
/**
* Created by hemanths on 2019-08-01.
*/
class MediaSessionCallback(
private val context: Context,
private val musicService: MusicService
) : MediaSessionCompat.Callback(), KoinComponent {
private val songRepository by inject<SongRepository>()
private val albumRepository by inject<AlbumRepository>()
private val artistRepository by inject<ArtistRepository>()
private val genreRepository by inject<GenreRepository>()
private val playlistRepository by inject<PlaylistRepository>()
private val topPlayedRepository by inject<TopPlayedRepository>()
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
super.onPlayFromMediaId(mediaId, extras)
val musicId = AutoMediaIDHelper.extractMusicID(mediaId!!)
println(musicId)
val itemId = musicId?.toLong() ?: -1
val songs: ArrayList<Song> = ArrayList()
when (val category = AutoMediaIDHelper.extractCategory(mediaId)) {
AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_ALBUM -> {
val album: Album = albumRepository.album(itemId)
songs.addAll(album.songs)
musicService.openQueue(songs, 0, true)
}
AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_ARTIST -> {
val artist: Artist = artistRepository.artist(itemId)
songs.addAll(artist.songs)
musicService.openQueue(songs, 0, true)
}
AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_ALBUM_ARTIST -> {
val artist: Artist =
artistRepository.albumArtist(albumRepository.album(itemId).albumArtist!!)
songs.addAll(artist.songs)
musicService.openQueue(songs, 0, true)
}
AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_PLAYLIST -> {
val playlist: Playlist = playlistRepository.playlist(itemId)
songs.addAll(playlist.getSongs())
musicService.openQueue(songs, 0, true)
}
AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_GENRE -> {
songs.addAll(genreRepository.songs(itemId))
musicService.openQueue(songs, 0, true)
}
AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_SHUFFLE -> {
val allSongs: ArrayList<Song> = songRepository.songs() as ArrayList<Song>
makeShuffleList(allSongs, -1)
musicService.openQueue(allSongs, 0, true)
}
AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_HISTORY,
AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_SUGGESTIONS,
AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_TOP_TRACKS,
AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_QUEUE -> {
val tracks: List<Song> = when (category) {
AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_HISTORY -> topPlayedRepository.recentlyPlayedTracks()
AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_SUGGESTIONS -> topPlayedRepository.recentlyPlayedTracks()
AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_TOP_TRACKS -> topPlayedRepository.recentlyPlayedTracks()
else -> musicService.playingQueue
}
songs.addAll(tracks)
var songIndex = MusicUtil.indexOfSongInList(tracks, itemId)
if (songIndex == -1) {
songIndex = 0
}
musicService.openQueue(songs, songIndex, true)
}
else -> {
}
}
musicService.play()
}
override fun onPlayFromSearch(query: String?, extras: Bundle?) {
val songs = ArrayList<Song>()
if (query.isNullOrEmpty()) {
// The user provided generic string e.g. 'Play music'
// Build appropriate playlist queue
songs.addAll(songRepository.songs())
} else {
// Build a queue based on songs that match "query" or "extras" param
val mediaFocus: String? = extras?.getString(MediaStore.EXTRA_MEDIA_FOCUS)
if (mediaFocus == MediaStore.Audio.Artists.ENTRY_CONTENT_TYPE) {
val artistQuery = extras.getString(MediaStore.EXTRA_MEDIA_ARTIST)
if (artistQuery != null) {
artistRepository.artists(artistQuery).forEach {
songs.addAll(it.songs)
}
}
} else if (mediaFocus == MediaStore.Audio.Albums.ENTRY_CONTENT_TYPE) {
val albumQuery = extras.getString(MediaStore.EXTRA_MEDIA_ALBUM)
if (albumQuery != null) {
albumRepository.albums(albumQuery).forEach {
songs.addAll(it.songs)
}
}
}
}
if (songs.isEmpty()) {
// No focus found, search by query for song title
query?.also {
songs.addAll(songRepository.songs(it))
}
}
musicService.openQueue(songs, 0, true)
musicService.play()
}
override fun onPlay() {
super.onPlay()
musicService.play()
}
override fun onPause() {
super.onPause()
musicService.pause()
}
override fun onSkipToNext() {
super.onSkipToNext()
musicService.playNextSong(true)
}
override fun onSkipToPrevious() {
super.onSkipToPrevious()
musicService.back(true)
}
override fun onStop() {
super.onStop()
musicService.quit()
}
override fun onSeekTo(pos: Long) {
super.onSeekTo(pos)
musicService.seek(pos.toInt())
}
override fun onCustomAction(action: String, extras: Bundle?) {
when (action) {
CYCLE_REPEAT -> {
cycleRepeatMode()
musicService.updateMediaSessionPlaybackState()
}
TOGGLE_SHUFFLE -> {
musicService.toggleShuffle()
musicService.updateMediaSessionPlaybackState()
}
TOGGLE_FAVORITE -> {
MusicUtil.toggleFavorite(context, MusicPlayerRemote.currentSong)
musicService.updateMediaSessionPlaybackState()
}
else -> {
println("Unsupported action: $action")
}
}
}
private fun checkAndStartPlaying(songs: ArrayList<Song>, itemId: Long) {
var songIndex = MusicUtil.indexOfSongInList(songs, itemId)
if (songIndex == -1) {
songIndex = 0
}
openQueue(songs, songIndex)
}
private fun openQueue(songs: ArrayList<Song>, index: Int, startPlaying: Boolean = true) {
MusicPlayerRemote.openQueue(songs, index, startPlaying)
}
}

View file

@ -0,0 +1,43 @@
/*
* 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 io.github.muntashirakon.music.service
import android.database.ContentObserver
import android.os.Handler
class MediaStoreObserver(
private val musicService: MusicService,
private val mHandler: Handler
) : ContentObserver(mHandler), Runnable {
override fun onChange(selfChange: Boolean) {
// if a change is detected, remove any scheduled callback
// then post a new one. This is intended to prevent closely
// spaced events from generating multiple refresh calls
mHandler.removeCallbacks(this)
mHandler.postDelayed(this, REFRESH_DELAY)
}
override fun run() {
// actually call refresh when the delayed callback fires
// do not send a sticky broadcast here
musicService.handleAndSendChangeInternal(MusicService.MEDIA_STORE_CHANGED)
}
companion object {
// milliseconds to delay before calling refresh to aggregate events
private const val REFRESH_DELAY: Long = 500
}
}

View file

@ -0,0 +1,368 @@
/*
* 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 io.github.muntashirakon.music.service;
import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.PlaybackParams;
import android.media.audiofx.AudioEffect;
import android.net.Uri;
import android.os.PowerManager;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.jetbrains.annotations.NotNull;
import code.name.monkey.appthemehelper.util.VersionUtils;
import io.github.muntashirakon.music.R;
import io.github.muntashirakon.music.service.playback.Playback;
import io.github.muntashirakon.music.util.PreferenceUtil;
/**
* @author Andrew Neal, Karim Abou Zeid (kabouzeid)
*/
public class MultiPlayer
implements Playback, MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener {
public static final String TAG = MultiPlayer.class.getSimpleName();
private MediaPlayer mCurrentMediaPlayer = new MediaPlayer();
private MediaPlayer mNextMediaPlayer;
private final Context context;
@Nullable
private Playback.PlaybackCallbacks callbacks;
private boolean mIsInitialized = false;
/**
* Constructor of <code>MultiPlayer</code>
*/
MultiPlayer(final Context context) {
this.context = context;
mCurrentMediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK);
}
/**
* @param path The path of the file, or the http/rtsp URL of the stream you want to play
* @return True if the <code>player</code> has been prepared and is ready to play, false otherwise
*/
@Override
public boolean setDataSource(@NotNull final String path, boolean force) {
mIsInitialized = false;
mIsInitialized = setDataSourceImpl(mCurrentMediaPlayer, path);
if (mIsInitialized) {
setNextDataSource(null);
}
return mIsInitialized;
}
/**
* @param player The {@link MediaPlayer} to use
* @param path The path of the file, or the http/rtsp URL of the stream you want to play
* @return True if the <code>player</code> has been prepared and is ready to play, false otherwise
*/
private boolean setDataSourceImpl(@NonNull final MediaPlayer player, @NonNull final String path) {
if (context == null) {
return false;
}
try {
player.reset();
player.setOnPreparedListener(null);
if (path.startsWith("content://")) {
player.setDataSource(context, Uri.parse(path));
} else {
player.setDataSource(path);
}
setPlaybackSpeedPitch(PreferenceUtil.INSTANCE.getPlaybackSpeed(), PreferenceUtil.INSTANCE.getPlaybackPitch());
player.setAudioStreamType(AudioManager.STREAM_MUSIC);
player.prepare();
} catch (Exception e) {
return false;
}
player.setOnCompletionListener(this);
player.setOnErrorListener(this);
final Intent intent = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION);
intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId());
intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.getPackageName());
intent.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC);
context.sendBroadcast(intent);
return true;
}
/**
* Set the MediaPlayer to start when this MediaPlayer finishes playback.
*
* @param path The path of the file, or the http/rtsp URL of the stream you want to play
*/
@Override
public void setNextDataSource(@Nullable final String path) {
if (context == null) {
return;
}
try {
mCurrentMediaPlayer.setNextMediaPlayer(null);
} catch (IllegalArgumentException e) {
Log.i(TAG, "Next media player is current one, continuing");
} catch (IllegalStateException e) {
Log.e(TAG, "Media player not initialized!");
return;
}
if (mNextMediaPlayer != null) {
mNextMediaPlayer.release();
mNextMediaPlayer = null;
}
if (path == null) {
return;
}
if (PreferenceUtil.INSTANCE.isGapLessPlayback()) {
mNextMediaPlayer = new MediaPlayer();
mNextMediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK);
mNextMediaPlayer.setAudioSessionId(getAudioSessionId());
if (setDataSourceImpl(mNextMediaPlayer, path)) {
try {
mCurrentMediaPlayer.setNextMediaPlayer(mNextMediaPlayer);
} catch (@NonNull IllegalArgumentException | IllegalStateException e) {
Log.e(TAG, "setNextDataSource: setNextMediaPlayer()", e);
if (mNextMediaPlayer != null) {
mNextMediaPlayer.release();
mNextMediaPlayer = null;
}
}
} else {
if (mNextMediaPlayer != null) {
mNextMediaPlayer.release();
mNextMediaPlayer = null;
}
}
}
}
/**
* Sets the callbacks
*
* @param callbacks The callbacks to use
*/
@Override
public void setCallbacks(@Nullable final Playback.PlaybackCallbacks callbacks) {
this.callbacks = callbacks;
}
/**
* @return True if the player is ready to go, false otherwise
*/
@Override
public boolean isInitialized() {
return mIsInitialized;
}
/**
* Starts or resumes playback.
*/
@Override
public boolean start() {
try {
mCurrentMediaPlayer.start();
return true;
} catch (IllegalStateException e) {
return false;
}
}
/**
* Resets the MediaPlayer to its uninitialized state.
*/
@Override
public void stop() {
mCurrentMediaPlayer.reset();
mIsInitialized = false;
}
/**
* Releases resources associated with this MediaPlayer object.
*/
@Override
public void release() {
stop();
mCurrentMediaPlayer.release();
if (mNextMediaPlayer != null) {
mNextMediaPlayer.release();
}
}
/**
* Pauses playback. Call start() to resume.
*/
@Override
public boolean pause() {
try {
mCurrentMediaPlayer.pause();
return true;
} catch (IllegalStateException e) {
return false;
}
}
/**
* Checks whether the MultiPlayer is playing.
*/
@Override
public boolean isPlaying() {
return mIsInitialized && mCurrentMediaPlayer.isPlaying();
}
/**
* Gets the duration of the file.
*
* @return The duration in milliseconds
*/
@Override
public int duration() {
if (!mIsInitialized) {
return -1;
}
try {
return mCurrentMediaPlayer.getDuration();
} catch (IllegalStateException e) {
return -1;
}
}
/**
* Gets the current playback position.
*
* @return The current position in milliseconds
*/
@Override
public int position() {
if (!mIsInitialized) {
return -1;
}
try {
return mCurrentMediaPlayer.getCurrentPosition();
} catch (IllegalStateException e) {
return -1;
}
}
/**
* Gets the current playback position.
*
* @param whereto The offset in milliseconds from the start to seek to
* @return The offset in milliseconds from the start to seek to
*/
@Override
public int seek(final int whereto) {
try {
mCurrentMediaPlayer.seekTo(whereto);
return whereto;
} catch (IllegalStateException e) {
return -1;
}
}
@Override
public boolean setVolume(final float vol) {
try {
mCurrentMediaPlayer.setVolume(vol, vol);
return true;
} catch (IllegalStateException e) {
return false;
}
}
/**
* Sets the audio session ID.
*
* @param sessionId The audio session ID
*/
@Override
public boolean setAudioSessionId(final int sessionId) {
try {
mCurrentMediaPlayer.setAudioSessionId(sessionId);
return true;
} catch (@NonNull IllegalArgumentException | IllegalStateException e) {
return false;
}
}
/**
* Returns the audio session ID.
*
* @return The current audio session ID.
*/
@Override
public int getAudioSessionId() {
return mCurrentMediaPlayer.getAudioSessionId();
}
/**
* {@inheritDoc}
*/
@Override
public boolean onError(final MediaPlayer mp, final int what, final int extra) {
mIsInitialized = false;
mCurrentMediaPlayer.release();
mCurrentMediaPlayer = new MediaPlayer();
mCurrentMediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK);
if (context != null) {
Toast.makeText(
context,
context.getResources().getString(R.string.unplayable_file),
Toast.LENGTH_SHORT)
.show();
Log.e(TAG, String.valueOf(what) + extra);
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
public void onCompletion(final MediaPlayer mp) {
if (mp.equals(mCurrentMediaPlayer) && mNextMediaPlayer != null) {
mIsInitialized = false;
mCurrentMediaPlayer.release();
mCurrentMediaPlayer = mNextMediaPlayer;
mIsInitialized = true;
mNextMediaPlayer = null;
if (callbacks != null) callbacks.onTrackWentToNext();
} else {
if (callbacks != null) callbacks.onTrackEnded();
}
}
@Override
public void setCrossFadeDuration(int duration) {
}
@Override
public void setPlaybackSpeedPitch(float speed, float pitch) {
if (VersionUtils.INSTANCE.hasMarshmallow()) {
boolean wasPlaying = mCurrentMediaPlayer.isPlaying();
mCurrentMediaPlayer.setPlaybackParams(new PlaybackParams()
.setSpeed(PreferenceUtil.INSTANCE.getPlaybackSpeed())
.setPitch(PreferenceUtil.INSTANCE.getPlaybackPitch()));
if (!wasPlaying) {
if (mCurrentMediaPlayer.isPlaying()) mCurrentMediaPlayer.pause();
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,175 @@
/*
* 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 io.github.muntashirakon.music.service;
import static io.github.muntashirakon.music.service.MusicService.DUCK;
import static io.github.muntashirakon.music.service.MusicService.META_CHANGED;
import static io.github.muntashirakon.music.service.MusicService.PLAY_STATE_CHANGED;
import static io.github.muntashirakon.music.service.MusicService.REPEAT_MODE_NONE;
import static io.github.muntashirakon.music.service.MusicService.TRACK_ENDED;
import static io.github.muntashirakon.music.service.MusicService.TRACK_WENT_TO_NEXT;
import android.media.AudioManager;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import androidx.annotation.NonNull;
import java.lang.ref.WeakReference;
import io.github.muntashirakon.music.util.PreferenceUtil;
class PlaybackHandler extends Handler {
@NonNull private final WeakReference<MusicService> mService;
private float currentDuckVolume = 1.0f;
PlaybackHandler(final MusicService service, @NonNull final Looper looper) {
super(looper);
mService = new WeakReference<>(service);
}
@Override
public void handleMessage(@NonNull final Message msg) {
final MusicService service = mService.get();
if (service == null) {
return;
}
switch (msg.what) {
case MusicService.DUCK:
if (PreferenceUtil.INSTANCE.isAudioDucking()) {
currentDuckVolume -= .05f;
if (currentDuckVolume > .2f) {
sendEmptyMessageDelayed(DUCK, 10);
} else {
currentDuckVolume = .2f;
}
} else {
currentDuckVolume = 1f;
}
service.playback.setVolume(currentDuckVolume);
break;
case MusicService.UNDUCK:
if (PreferenceUtil.INSTANCE.isAudioDucking()) {
currentDuckVolume += .03f;
if (currentDuckVolume < 1f) {
sendEmptyMessageDelayed(MusicService.UNDUCK, 10);
} else {
currentDuckVolume = 1f;
}
} else {
currentDuckVolume = 1f;
}
service.playback.setVolume(currentDuckVolume);
break;
case TRACK_WENT_TO_NEXT:
if (service.pendingQuit
|| service.getRepeatMode() == REPEAT_MODE_NONE && service.isLastTrack()) {
service.pause();
service.seek(0);
if (service.pendingQuit) {
service.pendingQuit = false;
service.quit();
break;
}
} else {
service.position = service.nextPosition;
service.prepareNextImpl();
service.notifyChange(META_CHANGED);
}
break;
case TRACK_ENDED:
// if there is a timer finished, don't continue
if (service.pendingQuit
|| service.getRepeatMode() == REPEAT_MODE_NONE && service.isLastTrack()) {
service.notifyChange(PLAY_STATE_CHANGED);
service.seek(0);
if (service.pendingQuit) {
service.pendingQuit = false;
service.quit();
break;
}
} else {
service.playNextSong(false);
}
sendEmptyMessage(MusicService.RELEASE_WAKELOCK);
break;
case MusicService.RELEASE_WAKELOCK:
service.releaseWakeLock();
break;
case MusicService.PLAY_SONG:
service.playSongAtImpl(msg.arg1);
break;
case MusicService.SET_POSITION:
service.openTrackAndPrepareNextAt(msg.arg1);
service.notifyChange(PLAY_STATE_CHANGED);
break;
case MusicService.PREPARE_NEXT:
service.prepareNextImpl();
break;
case MusicService.RESTORE_QUEUES:
service.restoreQueuesAndPositionIfNecessary();
break;
case MusicService.FOCUS_CHANGE:
switch (msg.arg1) {
case AudioManager.AUDIOFOCUS_GAIN:
if (!service.isPlaying() && service.isPausedByTransientLossOfFocus()) {
service.play();
service.setPausedByTransientLossOfFocus(false);
}
removeMessages(DUCK);
sendEmptyMessage(MusicService.UNDUCK);
break;
case AudioManager.AUDIOFOCUS_LOSS:
// Lost focus for an unbounded amount of time: stop playback and release media playback
boolean isAudioFocusEnabled = PreferenceUtil.INSTANCE.isAudioFocusEnabled();
if (!isAudioFocusEnabled) {
service.forcePause();
}
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
// Lost focus for a short time, but we have to stop
// playback. We don't release the media playback because playback
// is likely to resume
boolean wasPlaying = service.isPlaying();
service.forcePause();
service.setPausedByTransientLossOfFocus(wasPlaying);
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
// Lost focus for a short time, but it's ok to keep playing
// at an attenuated level
removeMessages(MusicService.UNDUCK);
sendEmptyMessage(DUCK);
break;
}
break;
}
}
}

View file

@ -0,0 +1,35 @@
/*
* 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 io.github.muntashirakon.music.service
import android.os.Handler
import android.os.Looper
import android.os.Message
import io.github.muntashirakon.music.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()
}
}
}

View file

@ -0,0 +1,50 @@
/*
* 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 io.github.muntashirakon.music.service
import io.github.muntashirakon.music.helper.StopWatch
import io.github.muntashirakon.music.model.Song
class SongPlayCountHelper {
private val stopWatch = StopWatch()
var song = Song.emptySong
private set
fun shouldBumpPlayCount(): Boolean {
return song.duration * 0.5 < stopWatch.elapsedTime
}
fun notifySongChanged(song: Song) {
synchronized(this) {
stopWatch.reset()
this.song = song
}
}
fun notifyPlayStateChanged(isPlaying: Boolean) {
synchronized(this) {
if (isPlaying) {
stopWatch.start()
} else {
stopWatch.pause()
}
}
}
companion object {
val TAG: String = SongPlayCountHelper::class.java.simpleName
}
}

View file

@ -0,0 +1,42 @@
/*
* 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 io.github.muntashirakon.music.service
import android.os.Handler
import io.github.muntashirakon.music.service.MusicService.Companion.PLAY_STATE_CHANGED
class ThrottledSeekHandler(
private val musicService: MusicService,
private val handler: Handler
) : Runnable {
fun notifySeek() {
musicService.updateMediaSessionPlaybackState()
musicService.updateMediaSessionMetaData()
handler.removeCallbacks(this)
handler.postDelayed(this, THROTTLE)
}
override fun run() {
musicService.savePositionInTrack()
musicService.sendPublicIntent(PLAY_STATE_CHANGED) // for musixmatch synced lyrics
}
companion object {
// milliseconds to throttle before calling run() to aggregate events
private const val THROTTLE: Long = 500
}
}

View file

@ -0,0 +1,65 @@
/*
* 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 io.github.muntashirakon.music.service.notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import io.github.muntashirakon.music.R
import io.github.muntashirakon.music.model.Song
abstract class PlayingNotification(context: Context) :
NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID) {
abstract fun updateMetadata(song: Song, onUpdate: () -> Unit)
abstract fun setPlaying(isPlaying: Boolean)
abstract fun updateFavorite(song: Song, onUpdate: () -> Unit)
companion object {
const val NOTIFICATION_CONTROLS_SIZE_MULTIPLIER = 1.0f
internal const val NOTIFICATION_CHANNEL_ID = "playing_notification"
const val NOTIFICATION_ID = 1
@RequiresApi(26)
fun createNotificationChannel(
context: Context,
notificationManager: NotificationManager
) {
var notificationChannel: NotificationChannel? = notificationManager
.getNotificationChannel(NOTIFICATION_CHANNEL_ID)
if (notificationChannel == null) {
notificationChannel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
context.getString(R.string.playing_notification_name),
NotificationManager.IMPORTANCE_LOW
)
notificationChannel.description =
context.getString(R.string.playing_notification_description)
notificationChannel.enableLights(false)
notificationChannel.enableVibration(false)
notificationChannel.setShowBadge(false)
notificationManager.createNotificationChannel(notificationChannel)
}
}
}
}

View file

@ -0,0 +1,304 @@
/*
* 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 io.github.muntashirakon.music.service.notification
import android.annotation.SuppressLint
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.widget.RemoteViews
import androidx.core.app.NotificationCompat
import androidx.core.graphics.drawable.toBitmap
import androidx.media.app.NotificationCompat.DecoratedMediaCustomViewStyle
import code.name.monkey.appthemehelper.util.ATHUtil.resolveColor
import code.name.monkey.appthemehelper.util.ColorUtil
import code.name.monkey.appthemehelper.util.MaterialValueHelper
import code.name.monkey.appthemehelper.util.VersionUtils
import io.github.muntashirakon.music.R
import io.github.muntashirakon.music.activities.MainActivity
import io.github.muntashirakon.music.extensions.getTintedDrawable
import io.github.muntashirakon.music.extensions.isColorLight
import io.github.muntashirakon.music.extensions.isSystemDarkModeEnabled
import io.github.muntashirakon.music.extensions.toBitmap
import io.github.muntashirakon.music.glide.GlideApp
import io.github.muntashirakon.music.glide.RetroGlideExtension
import io.github.muntashirakon.music.glide.palette.BitmapPaletteWrapper
import io.github.muntashirakon.music.model.Song
import io.github.muntashirakon.music.service.MusicService
import io.github.muntashirakon.music.service.MusicService.Companion.ACTION_QUIT
import io.github.muntashirakon.music.service.MusicService.Companion.ACTION_REWIND
import io.github.muntashirakon.music.service.MusicService.Companion.ACTION_SKIP
import io.github.muntashirakon.music.service.MusicService.Companion.ACTION_TOGGLE_PAUSE
import io.github.muntashirakon.music.util.PreferenceUtil
import io.github.muntashirakon.music.util.color.MediaNotificationProcessor
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
/**
* @author Hemanth S (h4h13).
*/
@SuppressLint("RestrictedApi")
class PlayingNotificationClassic(
val context: Context,
) : PlayingNotification(context) {
private var primaryColor: Int = 0
private fun getCombinedRemoteViews(collapsed: Boolean, song: Song): RemoteViews {
val remoteViews = RemoteViews(
context.packageName,
if (collapsed) R.layout.layout_notification_collapsed else R.layout.layout_notification_expanded
)
remoteViews.setTextViewText(
R.id.appName,
context.getString(R.string.app_name) + "" + song.albumName
)
remoteViews.setTextViewText(R.id.title, song.title)
remoteViews.setTextViewText(R.id.subtitle, song.artistName)
linkButtons(remoteViews)
return remoteViews
}
override fun updateMetadata(song: Song, onUpdate: () -> Unit) {
val notificationLayout = getCombinedRemoteViews(true, song)
val notificationLayoutBig = getCombinedRemoteViews(false, song)
val action = Intent(context, MainActivity::class.java)
action.putExtra(MainActivity.EXPAND_PANEL, PreferenceUtil.isExpandPanel)
action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
val clickIntent = PendingIntent
.getActivity(
context,
0,
action,
PendingIntent.FLAG_UPDATE_CURRENT or if (VersionUtils.hasMarshmallow())
PendingIntent.FLAG_IMMUTABLE
else 0
)
val deleteIntent = buildPendingIntent(context, ACTION_QUIT, null)
setSmallIcon(R.drawable.ic_notification)
setContentIntent(clickIntent)
setDeleteIntent(deleteIntent)
setCategory(NotificationCompat.CATEGORY_SERVICE)
priority = NotificationCompat.PRIORITY_MAX
setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
setCustomContentView(notificationLayout)
setCustomBigContentView(notificationLayoutBig)
setStyle(DecoratedMediaCustomViewStyle())
setOngoing(true)
val bigNotificationImageSize = context.resources
.getDimensionPixelSize(R.dimen.notification_big_image_size)
GlideApp.with(context).asBitmapPalette().songCoverOptions(song)
.load(RetroGlideExtension.getSongModel(song))
.centerCrop()
.into(object : CustomTarget<BitmapPaletteWrapper>(
bigNotificationImageSize,
bigNotificationImageSize
) {
override fun onResourceReady(
resource: BitmapPaletteWrapper,
transition: Transition<in BitmapPaletteWrapper>?,
) {
val colors = MediaNotificationProcessor(context, resource.bitmap)
update(resource.bitmap, colors.backgroundColor)
}
override fun onLoadFailed(errorDrawable: Drawable?) {
super.onLoadFailed(errorDrawable)
update(
null,
resolveColor(context, R.attr.colorSurface, Color.WHITE)
)
}
override fun onLoadCleared(placeholder: Drawable?) {
update(
null,
resolveColor(context, R.attr.colorSurface, Color.WHITE)
)
}
private fun update(bitmap: Bitmap?, bgColor: Int) {
var bgColorFinal = bgColor
if (bitmap != null) {
contentView.setImageViewBitmap(R.id.largeIcon, bitmap)
bigContentView.setImageViewBitmap(R.id.largeIcon, bitmap)
} else {
contentView.setImageViewResource(
R.id.largeIcon,
R.drawable.default_audio_art
)
bigContentView.setImageViewResource(
R.id.largeIcon,
R.drawable.default_audio_art
)
}
// Android 12 applies a standard Notification template to every notification
// which will in turn have a default background so setting a different background
// than that, looks weird
if (!VersionUtils.hasS()) {
if (!PreferenceUtil.isColoredNotification) {
bgColorFinal =
resolveColor(context, R.attr.colorSurface, Color.WHITE)
}
setBackgroundColor(bgColorFinal)
setNotificationContent(ColorUtil.isColorLight(bgColorFinal))
} else {
if (PreferenceUtil.isColoredNotification) {
setColorized(true)
color = bgColor
setNotificationContent(color.isColorLight)
} else {
setNotificationContent(!context.isSystemDarkModeEnabled())
}
}
onUpdate()
}
private fun setBackgroundColor(color: Int) {
contentView.setInt(R.id.image, "setBackgroundColor", color)
bigContentView.setInt(R.id.image, "setBackgroundColor", color)
}
private fun setNotificationContent(dark: Boolean) {
val primary = MaterialValueHelper.getPrimaryTextColor(context, dark)
val secondary = MaterialValueHelper.getSecondaryTextColor(context, dark)
primaryColor = primary
val close = context.getTintedDrawable(
R.drawable.ic_close,
primary
).toBitmap()
val prev =
context.getTintedDrawable(
R.drawable.ic_skip_previous_round_white_32dp,
primary
).toBitmap()
val next =
context.getTintedDrawable(
R.drawable.ic_skip_next_round_white_32dp,
primary
).toBitmap()
val playPause = getPlayPauseBitmap(true)
contentView.setTextColor(R.id.title, primary)
contentView.setTextColor(R.id.subtitle, secondary)
contentView.setTextColor(R.id.appName, secondary)
contentView.setImageViewBitmap(R.id.action_prev, prev)
contentView.setImageViewBitmap(R.id.action_next, next)
contentView.setImageViewBitmap(R.id.action_play_pause, playPause)
bigContentView.setTextColor(R.id.title, primary)
bigContentView.setTextColor(R.id.subtitle, secondary)
bigContentView.setTextColor(R.id.appName, secondary)
bigContentView.setImageViewBitmap(R.id.action_quit, close)
bigContentView.setImageViewBitmap(R.id.action_prev, prev)
bigContentView.setImageViewBitmap(R.id.action_next, next)
bigContentView.setImageViewBitmap(R.id.action_play_pause, playPause)
contentView.setImageViewBitmap(
R.id.smallIcon,
context.getTintedDrawable(
R.drawable.ic_notification,
secondary
).toBitmap(0.6f)
)
bigContentView.setImageViewBitmap(
R.id.smallIcon,
context.getTintedDrawable(
R.drawable.ic_notification,
secondary
).toBitmap(0.6f)
)
}
})
}
private fun getPlayPauseBitmap(isPlaying: Boolean): Bitmap {
return context.getTintedDrawable(
if (isPlaying)
R.drawable.ic_pause_white_48dp
else
R.drawable.ic_play_arrow_white_48dp, primaryColor
).toBitmap()
}
override fun setPlaying(isPlaying: Boolean) {
getPlayPauseBitmap(isPlaying).also {
contentView?.setImageViewBitmap(R.id.action_play_pause, it)
bigContentView?.setImageViewBitmap(R.id.action_play_pause, it)
}
}
override fun updateFavorite(song: Song, onUpdate: () -> Unit) {
}
private fun buildPendingIntent(
context: Context, action: String,
serviceName: ComponentName?,
): PendingIntent {
val intent = Intent(action)
intent.component = serviceName
return PendingIntent.getService(
context, 0, intent, if (VersionUtils.hasMarshmallow())
PendingIntent.FLAG_IMMUTABLE
else 0
)
}
private fun linkButtons(notificationLayout: RemoteViews) {
var pendingIntent: PendingIntent
val serviceName = ComponentName(context, MusicService::class.java)
// Previous track
pendingIntent = buildPendingIntent(context, ACTION_REWIND, serviceName)
notificationLayout.setOnClickPendingIntent(R.id.action_prev, pendingIntent)
// Play and pause
pendingIntent = buildPendingIntent(context, ACTION_TOGGLE_PAUSE, serviceName)
notificationLayout.setOnClickPendingIntent(R.id.action_play_pause, pendingIntent)
// Next track
pendingIntent = buildPendingIntent(context, ACTION_SKIP, serviceName)
notificationLayout.setOnClickPendingIntent(R.id.action_next, pendingIntent)
// Close
pendingIntent = buildPendingIntent(context, ACTION_QUIT, serviceName)
notificationLayout.setOnClickPendingIntent(R.id.action_quit, pendingIntent)
}
companion object {
fun from(
context: Context,
notificationManager: NotificationManager,
): PlayingNotification {
if (VersionUtils.hasOreo()) {
createNotificationChannel(context, notificationManager)
}
return PlayingNotificationClassic(context)
}
}
}

View file

@ -0,0 +1,221 @@
/*
* 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 io.github.muntashirakon.music.service.notification
import android.annotation.SuppressLint
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.drawable.Drawable
import android.support.v4.media.session.MediaSessionCompat
import androidx.core.app.NotificationCompat
import androidx.core.text.parseAsHtml
import androidx.media.app.NotificationCompat.MediaStyle
import code.name.monkey.appthemehelper.util.VersionUtils
import io.github.muntashirakon.music.R
import io.github.muntashirakon.music.activities.MainActivity
import io.github.muntashirakon.music.glide.GlideApp
import io.github.muntashirakon.music.glide.RetroGlideExtension
import io.github.muntashirakon.music.model.Song
import io.github.muntashirakon.music.service.MusicService
import io.github.muntashirakon.music.service.MusicService.Companion.ACTION_QUIT
import io.github.muntashirakon.music.service.MusicService.Companion.ACTION_REWIND
import io.github.muntashirakon.music.service.MusicService.Companion.ACTION_SKIP
import io.github.muntashirakon.music.service.MusicService.Companion.ACTION_TOGGLE_PAUSE
import io.github.muntashirakon.music.service.MusicService.Companion.TOGGLE_FAVORITE
import io.github.muntashirakon.music.util.MusicUtil
import io.github.muntashirakon.music.util.PreferenceUtil
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@SuppressLint("RestrictedApi")
class PlayingNotificationImpl24(
val context: Context,
mediaSessionToken: MediaSessionCompat.Token
) : PlayingNotification(context) {
init {
val action = Intent(context, MainActivity::class.java)
action.putExtra(MainActivity.EXPAND_PANEL, PreferenceUtil.isExpandPanel)
action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
val clickIntent =
PendingIntent.getActivity(
context,
0,
action,
PendingIntent.FLAG_UPDATE_CURRENT or if (VersionUtils.hasMarshmallow())
PendingIntent.FLAG_IMMUTABLE
else 0
)
val serviceName = ComponentName(context, MusicService::class.java)
val intent = Intent(ACTION_QUIT)
intent.component = serviceName
val deleteIntent = PendingIntent.getService(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or (if (VersionUtils.hasMarshmallow())
PendingIntent.FLAG_IMMUTABLE
else 0)
)
val toggleFavorite = buildFavoriteAction(false)
val playPauseAction = buildPlayAction(true)
val previousAction = NotificationCompat.Action(
R.drawable.ic_skip_previous_round_white_32dp,
context.getString(R.string.action_previous),
retrievePlaybackAction(ACTION_REWIND)
)
val nextAction = NotificationCompat.Action(
R.drawable.ic_skip_next_round_white_32dp,
context.getString(R.string.action_next),
retrievePlaybackAction(ACTION_SKIP)
)
val dismissAction = NotificationCompat.Action(
R.drawable.ic_close,
context.getString(R.string.action_cancel),
retrievePlaybackAction(ACTION_QUIT)
)
setSmallIcon(R.drawable.ic_notification)
setContentIntent(clickIntent)
setDeleteIntent(deleteIntent)
setShowWhen(false)
addAction(toggleFavorite)
addAction(previousAction)
addAction(playPauseAction)
addAction(nextAction)
if (VersionUtils.hasS()) {
addAction(dismissAction)
}
setStyle(
MediaStyle()
.setMediaSession(mediaSessionToken)
.setShowActionsInCompactView(1, 2, 3)
)
setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
}
override fun updateMetadata(song: Song, onUpdate: () -> Unit) {
setContentTitle(("<b>" + song.title + "</b>").parseAsHtml())
setContentText(song.artistName)
setSubText(("<b>" + song.albumName + "</b>").parseAsHtml())
val bigNotificationImageSize = context.resources
.getDimensionPixelSize(R.dimen.notification_big_image_size)
GlideApp.with(context)
.asBitmap()
.songCoverOptions(song)
.load(RetroGlideExtension.getSongModel(song))
//.checkIgnoreMediaStore()
.centerCrop()
.into(object : CustomTarget<Bitmap>(
bigNotificationImageSize,
bigNotificationImageSize
) {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
setLargeIcon(resource)
onUpdate()
}
override fun onLoadFailed(errorDrawable: Drawable?) {
super.onLoadFailed(errorDrawable)
setLargeIcon(
BitmapFactory.decodeResource(
context.resources,
R.drawable.default_audio_art
)
)
onUpdate()
}
override fun onLoadCleared(placeholder: Drawable?) {
setLargeIcon(
BitmapFactory.decodeResource(
context.resources,
R.drawable.default_audio_art
)
)
onUpdate()
}
})
}
private fun buildPlayAction(isPlaying: Boolean): NotificationCompat.Action {
val playButtonResId =
if (isPlaying) R.drawable.ic_pause_white_48dp else R.drawable.ic_play_arrow_white_48dp
return NotificationCompat.Action.Builder(
playButtonResId,
context.getString(R.string.action_play_pause),
retrievePlaybackAction(ACTION_TOGGLE_PAUSE)
).build()
}
private fun buildFavoriteAction(isFavorite: Boolean): NotificationCompat.Action {
val favoriteResId =
if (isFavorite) R.drawable.ic_favorite else R.drawable.ic_favorite_border
return NotificationCompat.Action.Builder(
favoriteResId,
context.getString(R.string.action_toggle_favorite),
retrievePlaybackAction(TOGGLE_FAVORITE)
).build()
}
override fun setPlaying(isPlaying: Boolean) {
mActions[2] = buildPlayAction(isPlaying)
}
override fun updateFavorite(song: Song, onUpdate: () -> Unit) {
GlobalScope.launch(Dispatchers.IO) {
val isFavorite = MusicUtil.repository.isSongFavorite(song.id)
withContext(Dispatchers.Main) {
mActions[0] = buildFavoriteAction(isFavorite)
onUpdate()
}
}
}
private fun retrievePlaybackAction(action: String): PendingIntent {
val serviceName = ComponentName(context, MusicService::class.java)
val intent = Intent(action)
intent.component = serviceName
return PendingIntent.getService(
context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or
if (VersionUtils.hasMarshmallow()) PendingIntent.FLAG_IMMUTABLE
else 0
)
}
companion object {
fun from(
context: Context,
notificationManager: NotificationManager,
mediaSession: MediaSessionCompat
): PlayingNotification {
if (VersionUtils.hasOreo()) {
createNotificationChannel(context, notificationManager)
}
return PlayingNotificationImpl24(context, mediaSession.sessionToken)
}
}
}

View file

@ -0,0 +1,61 @@
/*
* 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 io.github.muntashirakon.music.service.playback
interface Playback {
val isInitialized: Boolean
val isPlaying: Boolean
val audioSessionId: Int
fun setDataSource(path: String, force: Boolean): Boolean
fun setNextDataSource(path: String?)
fun setCallbacks(callbacks: PlaybackCallbacks)
fun start(): Boolean
fun stop()
fun release()
fun pause(): Boolean
fun duration(): Int
fun position(): Int
fun seek(whereto: Int): Int
fun setVolume(vol: Float): Boolean
fun setAudioSessionId(sessionId: Int): Boolean
fun setCrossFadeDuration(duration: Int)
fun setPlaybackSpeedPitch(speed: Float, pitch: Float)
interface PlaybackCallbacks {
fun onTrackWentToNext()
fun onTrackEnded()
fun onTrackEndedWithCrossfade()
}
}