498 lines
15 KiB
Kotlin
498 lines
15 KiB
Kotlin
/*
|
|
* Copyright (c) 2020 Hemanth Savarla.
|
|
*
|
|
* 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.helper
|
|
|
|
import android.annotation.TargetApi
|
|
import android.app.Activity
|
|
import android.content.*
|
|
import android.database.Cursor
|
|
import android.net.Uri
|
|
import android.os.Build
|
|
import android.os.Environment
|
|
import android.os.IBinder
|
|
import android.provider.DocumentsContract
|
|
import android.widget.Toast
|
|
import androidx.core.content.ContextCompat
|
|
import code.name.monkey.retromusic.R
|
|
import code.name.monkey.retromusic.model.Song
|
|
import code.name.monkey.retromusic.repository.SongRepository
|
|
import code.name.monkey.retromusic.service.MusicService
|
|
import code.name.monkey.retromusic.util.PreferenceUtil
|
|
import org.koin.core.component.KoinComponent
|
|
import org.koin.core.component.inject
|
|
import java.io.File
|
|
import java.util.*
|
|
|
|
|
|
object MusicPlayerRemote : KoinComponent {
|
|
val TAG: String = MusicPlayerRemote::class.java.simpleName
|
|
private val mConnectionMap = WeakHashMap<Context, ServiceBinder>()
|
|
var musicService: MusicService? = null
|
|
|
|
private val songRepository by inject<SongRepository>()
|
|
|
|
var isCasting: Boolean = false
|
|
set(value) {
|
|
field = value
|
|
if (value) {
|
|
musicService?.quit()
|
|
}
|
|
println(value.toString() + "" + isCasting.toString())
|
|
}
|
|
|
|
@JvmStatic
|
|
val isPlaying: Boolean
|
|
get() = musicService != null && musicService!!.isPlaying
|
|
|
|
fun isPlaying(song: Song): Boolean {
|
|
return if (!isPlaying) {
|
|
false
|
|
} else song.id == currentSong.id
|
|
}
|
|
|
|
val currentSong: Song
|
|
get() = if (musicService != null) {
|
|
musicService!!.currentSong
|
|
} else Song.emptySong
|
|
|
|
val nextSong: Song?
|
|
get() = if (musicService != null) {
|
|
musicService?.nextSong
|
|
} else Song.emptySong
|
|
|
|
/**
|
|
* Async
|
|
*/
|
|
var position: Int
|
|
get() = if (musicService != null) {
|
|
musicService!!.position
|
|
} else -1
|
|
set(position) {
|
|
if (musicService != null) {
|
|
musicService!!.position = position
|
|
}
|
|
}
|
|
|
|
@JvmStatic
|
|
val playingQueue: List<Song>
|
|
get() = if (musicService != null) {
|
|
musicService?.playingQueue as List<Song>
|
|
} else listOf()
|
|
|
|
val songProgressMillis: Int
|
|
get() = if (musicService != null) {
|
|
musicService!!.songProgressMillis
|
|
} else -1
|
|
|
|
val songDurationMillis: Int
|
|
get() = if (musicService != null) {
|
|
musicService!!.songDurationMillis
|
|
} else -1
|
|
|
|
val repeatMode: Int
|
|
get() = if (musicService != null) {
|
|
musicService!!.repeatMode
|
|
} else MusicService.REPEAT_MODE_NONE
|
|
|
|
@JvmStatic
|
|
val shuffleMode: Int
|
|
get() = if (musicService != null) {
|
|
musicService!!.shuffleMode
|
|
} else MusicService.SHUFFLE_MODE_NONE
|
|
|
|
val audioSessionId: Int
|
|
get() = if (musicService != null) {
|
|
musicService!!.audioSessionId
|
|
} else -1
|
|
|
|
val isServiceConnected: Boolean
|
|
get() = musicService != null
|
|
|
|
fun bindToService(context: Context, callback: ServiceConnection): ServiceToken? {
|
|
|
|
var realActivity: Activity? = (context as Activity).parent
|
|
if (realActivity == null) {
|
|
realActivity = context
|
|
}
|
|
|
|
val contextWrapper = ContextWrapper(realActivity)
|
|
val intent = Intent(contextWrapper, MusicService::class.java)
|
|
try {
|
|
contextWrapper.startService(intent)
|
|
} catch (ignored: IllegalStateException) {
|
|
ContextCompat.startForegroundService(context, intent)
|
|
}
|
|
val binder = ServiceBinder(callback)
|
|
|
|
if (contextWrapper.bindService(
|
|
Intent().setClass(contextWrapper, MusicService::class.java),
|
|
binder,
|
|
Context.BIND_AUTO_CREATE
|
|
)
|
|
) {
|
|
mConnectionMap[contextWrapper] = binder
|
|
return ServiceToken(contextWrapper)
|
|
}
|
|
return null
|
|
}
|
|
|
|
fun unbindFromService(token: ServiceToken?) {
|
|
if (token == null) {
|
|
return
|
|
}
|
|
val mContextWrapper = token.mWrappedContext
|
|
val mBinder = mConnectionMap.remove(mContextWrapper) ?: return
|
|
mContextWrapper.unbindService(mBinder)
|
|
if (mConnectionMap.isEmpty()) {
|
|
musicService = null
|
|
}
|
|
}
|
|
|
|
private fun getFilePathFromUri(context: Context, uri: Uri): String? {
|
|
var cursor: Cursor? = null
|
|
val column = "_data"
|
|
val projection = arrayOf(column)
|
|
|
|
try {
|
|
cursor = context.contentResolver.query(uri, projection, null, null, null)
|
|
if (cursor != null && cursor.moveToFirst()) {
|
|
val columnIndex = cursor.getColumnIndexOrThrow(column)
|
|
return cursor.getString(columnIndex)
|
|
}
|
|
} catch (e: Exception) {
|
|
println(e.message)
|
|
} finally {
|
|
cursor?.close()
|
|
}
|
|
return null
|
|
}
|
|
|
|
fun getQueueDurationSongs(): Int {
|
|
return musicService?.playingQueue?.size ?: -1
|
|
}
|
|
|
|
/**
|
|
* Async
|
|
*/
|
|
fun playSongAt(position: Int) {
|
|
musicService?.playSongAt(position)
|
|
}
|
|
|
|
fun pauseSong() {
|
|
musicService?.pause()
|
|
}
|
|
|
|
/**
|
|
* Async
|
|
*/
|
|
fun playNextSong() {
|
|
musicService?.playNextSong(true)
|
|
}
|
|
|
|
/**
|
|
* Async
|
|
*/
|
|
fun playPreviousSong() {
|
|
musicService?.playPreviousSong(true)
|
|
}
|
|
|
|
/**
|
|
* Async
|
|
*/
|
|
fun back() {
|
|
musicService?.back(true)
|
|
}
|
|
|
|
fun resumePlaying() {
|
|
musicService?.play()
|
|
}
|
|
|
|
/**
|
|
* Async
|
|
*/
|
|
@JvmStatic
|
|
fun openQueue(queue: List<Song>, startPosition: Int, startPlaying: Boolean) {
|
|
if (!tryToHandleOpenPlayingQueue(
|
|
queue,
|
|
startPosition,
|
|
startPlaying
|
|
) && musicService != null
|
|
) {
|
|
musicService?.openQueue(queue, startPosition, startPlaying)
|
|
if (PreferenceUtil.isShuffleModeOn)
|
|
setShuffleMode(MusicService.SHUFFLE_MODE_NONE)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Async
|
|
*/
|
|
@JvmStatic
|
|
fun openAndShuffleQueue(queue: List<Song>, startPlaying: Boolean) {
|
|
var startPosition = 0
|
|
if (queue.isNotEmpty()) {
|
|
startPosition = Random().nextInt(queue.size)
|
|
}
|
|
|
|
if (!tryToHandleOpenPlayingQueue(
|
|
queue,
|
|
startPosition,
|
|
startPlaying
|
|
) && musicService != null
|
|
) {
|
|
openQueue(queue, startPosition, startPlaying)
|
|
setShuffleMode(MusicService.SHUFFLE_MODE_SHUFFLE)
|
|
}
|
|
}
|
|
|
|
private fun tryToHandleOpenPlayingQueue(
|
|
queue: List<Song>,
|
|
startPosition: Int,
|
|
startPlaying: Boolean
|
|
): Boolean {
|
|
if (playingQueue === queue) {
|
|
if (startPlaying) {
|
|
playSongAt(startPosition)
|
|
} else {
|
|
position = startPosition
|
|
}
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
fun getQueueDurationMillis(position: Int): Long {
|
|
return if (musicService != null) {
|
|
musicService!!.getQueueDurationMillis(position)
|
|
} else -1
|
|
}
|
|
|
|
fun seekTo(millis: Int): Int {
|
|
return if (musicService != null) {
|
|
musicService!!.seek(millis)
|
|
} else -1
|
|
}
|
|
|
|
fun cycleRepeatMode(): Boolean {
|
|
if (musicService != null) {
|
|
musicService?.cycleRepeatMode()
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
fun toggleShuffleMode(): Boolean {
|
|
if (musicService != null) {
|
|
musicService?.toggleShuffle()
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
fun setShuffleMode(shuffleMode: Int): Boolean {
|
|
if (musicService != null) {
|
|
musicService!!.shuffleMode = shuffleMode
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
fun playNext(song: Song): Boolean {
|
|
if (musicService != null) {
|
|
if (playingQueue.isNotEmpty()) {
|
|
musicService?.addSong(position + 1, song)
|
|
} else {
|
|
val queue = ArrayList<Song>()
|
|
queue.add(song)
|
|
openQueue(queue, 0, false)
|
|
}
|
|
Toast.makeText(
|
|
musicService,
|
|
musicService!!.resources.getString(R.string.added_title_to_playing_queue),
|
|
Toast.LENGTH_SHORT
|
|
).show()
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
fun playNext(songs: List<Song>): Boolean {
|
|
if (musicService != null) {
|
|
if (playingQueue.isNotEmpty()) {
|
|
musicService?.addSongs(position + 1, songs)
|
|
} else {
|
|
openQueue(songs, 0, false)
|
|
}
|
|
val toast =
|
|
if (songs.size == 1) musicService!!.resources.getString(R.string.added_title_to_playing_queue) else musicService!!.resources.getString(
|
|
R.string.added_x_titles_to_playing_queue,
|
|
songs.size
|
|
)
|
|
Toast.makeText(musicService, toast, Toast.LENGTH_SHORT).show()
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
fun enqueue(song: Song): Boolean {
|
|
if (musicService != null) {
|
|
if (playingQueue.isNotEmpty()) {
|
|
musicService?.addSong(song)
|
|
} else {
|
|
val queue = ArrayList<Song>()
|
|
queue.add(song)
|
|
openQueue(queue, 0, false)
|
|
}
|
|
Toast.makeText(
|
|
musicService,
|
|
musicService!!.resources.getString(R.string.added_title_to_playing_queue),
|
|
Toast.LENGTH_SHORT
|
|
).show()
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
fun enqueue(songs: List<Song>): Boolean {
|
|
if (musicService != null) {
|
|
if (playingQueue.isNotEmpty()) {
|
|
musicService?.addSongs(songs)
|
|
} else {
|
|
openQueue(songs, 0, false)
|
|
}
|
|
val toast =
|
|
if (songs.size == 1) musicService!!.resources.getString(R.string.added_title_to_playing_queue) else musicService!!.resources.getString(
|
|
R.string.added_x_titles_to_playing_queue,
|
|
songs.size
|
|
)
|
|
Toast.makeText(musicService, toast, Toast.LENGTH_SHORT).show()
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
@JvmStatic
|
|
fun removeFromQueue(song: Song): Boolean {
|
|
if (musicService != null) {
|
|
musicService!!.removeSong(song)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
@JvmStatic
|
|
fun removeFromQueue(songs: List<Song>): Boolean {
|
|
if (musicService != null) {
|
|
musicService!!.removeSongs(songs)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
fun removeFromQueue(position: Int): Boolean {
|
|
if (musicService != null && position >= 0 && position < playingQueue.size) {
|
|
musicService!!.removeSong(position)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
fun moveSong(from: Int, to: Int): Boolean {
|
|
if (musicService != null && from >= 0 && to >= 0 && from < playingQueue.size && to < playingQueue.size) {
|
|
musicService!!.moveSong(from, to)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
fun clearQueue(): Boolean {
|
|
if (musicService != null) {
|
|
musicService!!.clearQueue()
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
@JvmStatic
|
|
fun playFromUri(uri: Uri) {
|
|
if (musicService != null) {
|
|
|
|
var songs: List<Song>? = null
|
|
if (uri.scheme != null && uri.authority != null) {
|
|
if (uri.scheme == ContentResolver.SCHEME_CONTENT) {
|
|
var songId: String? = null
|
|
if (uri.authority == "com.android.providers.media.documents") {
|
|
songId = getSongIdFromMediaProvider(uri)
|
|
} else if (uri.authority == "media") {
|
|
songId = uri.lastPathSegment
|
|
}
|
|
if (songId != null) {
|
|
songs = songRepository.songs(songId)
|
|
}
|
|
}
|
|
}
|
|
if (songs == null) {
|
|
var songFile: File? = null
|
|
if (uri.authority != null && uri.authority == "com.android.externalstorage.documents") {
|
|
songFile = File(
|
|
Environment.getExternalStorageDirectory(),
|
|
uri.path?.split(":".toRegex(), 2)?.get(1)
|
|
)
|
|
}
|
|
if (songFile == null) {
|
|
val path = getFilePathFromUri(musicService!!, uri)
|
|
if (path != null)
|
|
songFile = File(path)
|
|
}
|
|
if (songFile == null && uri.path != null) {
|
|
songFile = File(uri.path)
|
|
}
|
|
if (songFile != null) {
|
|
songs = songRepository.songsByFilePath(songFile.absolutePath)
|
|
}
|
|
}
|
|
if (songs != null && songs.isNotEmpty()) {
|
|
openQueue(songs, 0, true)
|
|
} else {
|
|
// TODO the file is not listed in the media store
|
|
println("The file is not listed in the media store")
|
|
}
|
|
}
|
|
}
|
|
|
|
@TargetApi(Build.VERSION_CODES.KITKAT)
|
|
private fun getSongIdFromMediaProvider(uri: Uri): String {
|
|
return DocumentsContract.getDocumentId(uri).split(":".toRegex())
|
|
.dropLastWhile { it.isEmpty() }.toTypedArray()[1]
|
|
}
|
|
|
|
class ServiceBinder internal constructor(private val mCallback: ServiceConnection?) :
|
|
ServiceConnection {
|
|
|
|
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
|
val binder = service as MusicService.MusicBinder
|
|
musicService = binder.service
|
|
mCallback?.onServiceConnected(className, service)
|
|
}
|
|
|
|
override fun onServiceDisconnected(className: ComponentName) {
|
|
mCallback?.onServiceDisconnected(className)
|
|
musicService = null
|
|
}
|
|
}
|
|
|
|
class ServiceToken internal constructor(internal var mWrappedContext: ContextWrapper)
|
|
}
|