##音楽アプリを作りたい
音楽アプリを作りたいんだけど、MediaSessionとかMediaBrowserServiceとかメンドクサイ。秒で音楽を再生できるようにしたい。という人向けの記事です。自分のメモ的な面もあるので、不備があればコメントお願いします。質問されればいつでも答える所存。
##秒で基本的な機能を実装したい
###その1
MusicClassとMusicServiceという名でktファイルを作成。(マニフェストに登録とかはすっ飛ばします)
###その2
以下MusicClassのソースコード。これをコピーして貼り付け。
(自分のアプリからそのまま引っ張ってきているのでエラー多いかも。使わないなと思ったところは消して構わない。気にすることなかれ。)
class MusicClass(_context: Context) {
private val context = _context
private var service: MediaBrowserServiceCompat? = null
private var musicMap = mutableMapOf<String, MusicData>()
private val artworkMap = mutableMapOf<String, Bitmap?>()
private var currentMusicData: MusicData?
set(value) = settings.setString(SETTING_MUSIC_VALUE, "CurrentMusic", value?.musicId)
get() = musicMap[settings.getString(SETTING_MUSIC_VALUE, "CurrentMusic", null)]
private var currentMusicState: Int = 1
set(value) {
field = value
callbacks.forEach { it.onUpdatePlayerState(value) }
}
private var queueIndex = -1
private var queueItems = mutableListOf<MediaSessionCompat.QueueItem>()
private var queueItemsOriginal = mutableListOf<MediaSessionCompat.QueueItem>()
private var initMusicClassFlag = false
private var initFirstConnection = false
private var initMusicBrowserFlag = false
private var tryConnectFlag = true
private val audioManager: AudioManager by lazy { context.getSystemService(Context.AUDIO_SERVICE) as AudioManager }
private val defaultArtwork: Bitmap by lazy { BitmapFactory.decodeResource(context.resources, R.drawable.album_art) }
private lateinit var musicPlayer: MediaPlayer
private lateinit var mediaSession: MediaSessionCompat
private lateinit var mediaBrowser: MediaBrowserCompat
private lateinit var mediaController: MediaControllerCompat
private var listener: MusicListener? = null
private var onCurrentMusicChangedListener: OnCurrentMusicChanged? = null
private var callbacks = mutableListOf<MusicCallback>()
private var reserveAction: (() -> Unit?)? = null
private val mediaControllerCallback = object : MediaControllerCompat.Callback() {
override fun onPlaybackStateChanged(state: PlaybackStateCompat?) {
super.onPlaybackStateChanged(state)
global.log(MUSIC_TAG, "MusicClass onPlaybackStateChanged")
if (state != null) {
callbacks.forEach { it.onUpdatePlayerState(state.state) }
}
}
override fun onMetadataChanged(metadata: MediaMetadataCompat?) {
super.onMetadataChanged(metadata)
global.log(MUSIC_TAG, "MusicClass onMetadataChanged")
if (metadata != null) {
callbacks.forEach { it.onUpdateMusicMetadata(metadata) }
}
}
}
private val mediaSessionCallback = object : MediaSessionCompat.Callback() {
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
currentMusicState = PlaybackStateCompat.STATE_PLAYING
musicMap[mediaId]?.let {
mediaSession.setMetadata(getMusicMetadata(mediaId))
listener?.onPlayFromMusicData(it)
}
}
override fun onPlay() {
listener?.onPlay()
currentMusicState = PlaybackStateCompat.STATE_PLAYING
}
override fun onPause() {
listener?.onPause()
currentMusicState = PlaybackStateCompat.STATE_PAUSED
}
override fun onStop() {
listener?.onStop()
currentMusicState = PlaybackStateCompat.STATE_STOPPED
}
override fun onSkipToNext() {
if (queueItems.isNotEmpty()) {
queueIndex = if (queueIndex + 1 >= queueItems.size) 0 else queueIndex + 1
val musicData = musicMap[queueItems[queueIndex].description.mediaId] ?: return
listener?.onSkipToNext(musicData, currentMusicState == PlaybackStateCompat.STATE_PLAYING)
}
}
override fun onSkipToPrevious() {
if (queueItems.isNotEmpty()) {
if ((settings.currentMusicProgress.toInt() / 1000) > 4 && settings.pGetBoolean("pOneStepBack", true)) {
listener?.onSeekTo(0)
} else {
queueIndex = if (queueIndex <= 0) queueItems.size - 1 else queueIndex - 1
val musicData = musicMap[queueItems[queueIndex].description.mediaId] ?: return
listener?.onSkipToPrevious(musicData, currentMusicState == PlaybackStateCompat.STATE_PLAYING)
}
}
}
override fun onSetShuffleMode(shuffleMode: Int) {
if (shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_NONE) {
settings.shuffleMode = false
queueItems = queueItemsOriginal.toMutableList()
} else {
settings.shuffleMode = true
val currentQueueItem = queueItems[queueIndex]
val tempQueueList = mutableListOf<MediaSessionCompat.QueueItem>()
queueItems.forEach {
if (it.description.mediaId != currentMusicData?.musicId) {
tempQueueList.add(it)
}
}
queueItems.clear()
queueItems.add(currentQueueItem)
queueItems.addAll(tempQueueList.shuffled())
}
currentMusicData?.let { updateQueueIndex(it) }
}
override fun onSetRepeatMode(repeatMode: Int) {
settings.repeatMode = repeatMode
}
override fun onSeekTo(pos: Long) {
listener?.onSeekTo(pos)
}
override fun onMediaButtonEvent(mediaButtonEvent: Intent?): Boolean {
val key: KeyEvent? = mediaButtonEvent!!.getParcelableExtra(Intent.EXTRA_KEY_EVENT)
global.log(MUSIC_TAG, "MusicClass onMediaButtonEvent: ${key?.keyCode}")
return when (key?.keyCode) {
KeyEvent.KEYCODE_MEDIA_PLAY -> {
currentMusicData?.let { this@MusicClass.onPlay(it, null) }
true
}
KeyEvent.KEYCODE_MEDIA_PAUSE -> {
this@MusicClass.onPause()
true
}
KeyEvent.KEYCODE_MEDIA_NEXT -> {
this@MusicClass.onSkipToNext()
true
}
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> {
this@MusicClass.onSkipToPrevious()
true
}
else -> super.onMediaButtonEvent(mediaButtonEvent)
}
}
}
private val connectionCallback = object : MediaBrowserCompat.ConnectionCallback() {
override fun onConnected() {
global.log(MUSIC_TAG, "MusicClass onConnected: server connected.")
mediaController = MediaControllerCompat(context, mediaBrowser.sessionToken)
mediaController.registerCallback(mediaControllerCallback)
initQueueItems()
initMusicBrowserFlag = true
initFirstConnection = true
tryConnectFlag = true
callbacks.forEach { it.onMusicPlayerConnected() }
reserveAction?.let { it() }
reserveAction = null
}
override fun onConnectionFailed() {
tryConnectFlag = true
global.log(MUSIC_TAG, "MusicClass onConnectionFailed: server connection failed.")
}
override fun onConnectionSuspended() {
tryConnectFlag = true
global.log(MUSIC_TAG, "MusicClass onConnectionSuspended: server connection suspended.")
}
}
init {
initialize()
}
fun connect() {
if (!initMusicClassFlag) {
initialize()
}
if (!initMusicBrowserFlag && tryConnectFlag) {
global.log(MUSIC_TAG, "connecting...")
tryConnectFlag = false
mediaBrowser.connect()
} else if (initMusicBrowserFlag) {
global.log(MUSIC_TAG, "server already connected.")
callbacks.forEach { it.onMusicPlayerConnected() }
} else global.log(MUSIC_TAG, "Reject connection.")
}
fun disconnect() {
mediaController.unregisterCallback(mediaControllerCallback)
mediaBrowser.disconnect()
callbacks.forEach { it.onMusicPlayerDisconnected() }
initMusicBrowserFlag = false
tryConnectFlag = true
global.log(MUSIC_TAG, "disconnected.")
}
fun attach(service: MediaBrowserServiceCompat, musicPlayer: MediaPlayer) {
global.log(MUSIC_TAG, "MusicClass attach: service attach.")
this.service = service
this.musicPlayer = musicPlayer
music.mediaSession = MediaSessionCompat(service.baseContext, MusicService::class.java.simpleName).apply {
setPlaybackState(
PlaybackStateCompat.Builder()
.setActions(PlaybackStateCompat.ACTION_PLAY or PlaybackStateCompat.ACTION_PAUSE or PlaybackStateCompat.ACTION_SKIP_TO_NEXT or PlaybackStateCompat.ACTION_STOP)
.build()
)
setCallback(music.mediaSessionCallback)
service.sessionToken = sessionToken
}
}
fun detach() {
global.log(MUSIC_TAG, "MusicClass detach: service detach.")
this.service = null
}
private fun initialize() {
global.log(MUSIC_TAG, "MusicClass initialize: Server is initializing...")
getMusic()
mediaBrowser = MediaBrowserCompat(context, ComponentName(context, MusicService::class.java), connectionCallback, null)
initMusicClassFlag = true
}
fun clearData() {
musicMap.clear()
artworkMap.clear()
}
fun reserveMusicAction(action: () -> (Unit)) {
if (!initMusicBrowserFlag) reserveAction = action
else action()
}
fun setCurrentMusic(newData: MusicData) {
global.log(MUSIC_TAG, "MusicClass setCurrentMusic: change current music data.")
currentMusicData = newData
onCurrentMusicChangedListener?.onCurrentMusicChanged(newData)
callbacks.forEach { it.onUpdateCurrentMusicData(newData) }
}
fun isConnect() = initMusicBrowserFlag
fun getCurrentMusic() = currentMusicData
fun getCurrentMusicState() = currentMusicState
fun getMusicMap() = musicMap
fun getQueueList() = queueItems
fun getQueueIndex() = queueIndex
fun getArtworkFromMap(albumName: String?) = artworkMap[albumName]
fun onPlay(musicData: MusicData, queueList: List<MusicData>?, onShuffle: Boolean = true, repeat: Boolean = false) {
if (!isConnect()) {
reserveMusicAction { onPlay(musicData, queueList, onShuffle, repeat) }
connect()
} else if (currentMusicData?.musicId != musicData.musicId || currentMusicState == 1 || currentMusicState == PlaybackStateCompat.STATE_STOPPED || repeat) {
if (queueList != null) setQueueItemList(musicData, queueList, onShuffle)
saveQueueItem()
mediaController.transportControls.playFromMediaId(musicData.musicId, null)
} else {
saveQueueItem()
mediaController.transportControls.play()
}
}
fun onPause() {
if (!isConnect()) {
reserveMusicAction { onPause() }
connect()
} else mediaController.transportControls.pause()
}
fun onStop() {
mediaController.transportControls.stop()
}
fun onSkipToNext() {
if (!isConnect()) {
reserveMusicAction { onSkipToNext() }
connect()
} else mediaController.transportControls.skipToNext()
}
fun onSkipToPrevious() {
if (!isConnect()) {
reserveMusicAction { onSkipToPrevious() }
connect()
} else mediaController.transportControls.skipToPrevious()
}
fun onShuffle(mode: Int) {
if (!isConnect()) {
reserveMusicAction { onShuffle(mode) }
connect()
} else mediaController.transportControls.setShuffleMode(mode)
}
fun onRepeat(mode: Int) {
if (!isConnect()) {
reserveMusicAction { onRepeat(mode) }
connect()
} else mediaController.transportControls.setRepeatMode(mode)
}
fun onSeek(progress: Long) {
if (!isConnect()) {
reserveMusicAction { onSeek(progress) }
connect()
} else mediaController.transportControls.seekTo(progress)
}
fun addCallback(callback: MusicCallback) {
this.callbacks.add(callback)
}
fun removeCallback(callback: MusicCallback) {
this.callbacks.remove(callback)
}
fun setListener(listener: MusicListener) {
this.listener = listener
}
fun removeListener() {
this.listener = null
}
fun setCurrentMusicChangeListener(listener: OnCurrentMusicChanged) {
this.onCurrentMusicChangedListener = listener
}
fun removeCurrentMusicChangeListener() {
this.onCurrentMusicChangedListener = null
}
fun setVolume(volumeLeft: Float, volumeRight: Float) {
callbacks.forEach { it.onUpdatePlayerVolume(volumeLeft, volumeRight) }
}
fun resetVolume() {
val baseAudioVolume = settings.pGetInt("qBaseVolume", 5).toFloat() / 10f
callbacks.forEach { it.onUpdatePlayerVolume(baseAudioVolume, baseAudioVolume) }
}
fun setBass(strength: Int) {
callbacks.forEach { it.onUpdatePlayerBass((strength * 50).toShort()) }
}
fun resetBass() {
callbacks.forEach { it.onUpdatePlayerBass(0) }
}
fun setEqualizer(hzMap: Pair<Int, Int>) {
callbacks.forEach { it.onUpdatePlayerEqualizer(hzMap) }
}
fun resetEqualizer() {
global.equalizerBandMap.forEach { band ->
callbacks.forEach { it.onUpdatePlayerEqualizer(Pair(band.key, 0)) }
}
}
fun setReverbEffect(effect: Short) {
callbacks.forEach { it.onUpdatePlayerReverbEffect(effect) }
}
fun progressUpdate(progress: Long) {
settings.currentMusicProgress = progress
callbacks.forEach { it.onUpdateMusicProgress(progress) }
}
fun setMasterMute(isMute: Boolean): Boolean {
val audioMethods = audioManager.javaClass.declaredMethods
for(method in audioMethods){
if(method.name == "setMasterMute"){
try {
method.invoke(audioManager, isMute, 0)
Log.d(MUSIC_TAG, "set master mute success. STATUS: $isMute")
return true
}
catch (e: Exception){
global.stackTrace(e.toString())
}
}
}
Log.d(MUSIC_TAG, "set master mute failed. STATUS: $isMute")
return false
}
fun isMasterMute(): Boolean {
val audioMethods = audioManager.javaClass.declaredMethods
for(method in audioMethods){
if(method.name == "isMasterMute"){
try {
val status = method.invoke(audioManager) as Boolean
Log.d(MUSIC_TAG, "get master mute success. STATUS: $status")
return status
}
catch (e: Exception){
global.stackTrace(e.toString())
}
}
}
Log.d(MUSIC_TAG, "set master mute failed.")
return false
}
fun getMusic() {
var cursor: Cursor? = null
val tempMap = mutableMapOf<String, MusicData>()
try {
cursor = context.contentResolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null, null, "title")
if (cursor != null && cursor.moveToFirst()) {
val artistColumn: Int = cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST)
val titleColumn: Int = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE)
val albumColumn: Int = cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM)
val albumIdColumn: Int = cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM_ID)
val durationColumn: Int = cursor.getColumnIndex(MediaStore.Audio.Media.DURATION)
val idColumn: Int = cursor.getColumnIndex(MediaStore.Audio.Media._ID)
val idTruck: Int = cursor.getColumnIndex(MediaStore.Audio.Media.TRACK)
val pathColumn: Int = cursor.getColumnIndex(MediaStore.Audio.Media.DATA)
val yearColumn = cursor.getColumnIndex(MediaStore.Audio.Media.YEAR)
do {
if (cursor.getInt(durationColumn) > 3000) {
val contentUri = Uri.withAppendedPath(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, cursor.getInt(idColumn).toString())
tempMap[cursor.getInt(idColumn).toString()] = MusicData(
cursor.getInt(idColumn).toString(),
cursor.getString(pathColumn),
null,
cursor.getString(titleColumn),
cursor.getString(artistColumn),
cursor.getString(albumColumn),
cursor.getLong(albumIdColumn),
cursor.getInt(durationColumn),
cursor.getString(idTruck),
cursor.getInt(yearColumn).toString(),
null,
contentUri
)
}
} while (cursor.moveToNext())
}
} catch (e: Exception) {
e.printStackTrace()
}
cursor?.close()
musicMap = tempMap
}
fun getMusicMediaItem(_musicData: MusicData?, musicId: String? = null): MediaBrowserCompat.MediaItem {
val musicData = _musicData ?: musicMap[musicId] ?: throw IllegalStateException("Cannot found music data.")
val metadata = MediaMetadataCompat.Builder().putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, musicData.musicId)
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, musicData.title).putString(MediaMetadataCompat.METADATA_KEY_ARTIST, musicData.artist)
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, musicData.album)
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, musicData.duration.toLong())
.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, musicData.albumArt).build()
return MediaBrowserCompat.MediaItem(metadata.description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
}
fun getMusicMetadata(id: String?): MediaMetadataCompat? {
id ?: return null
val musicData = musicMap[id] ?: return null
return MediaMetadataCompat.Builder().putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, musicData.musicId)
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, musicData.title).putString(MediaMetadataCompat.METADATA_KEY_ARTIST, musicData.artist)
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, musicData.album)
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, musicData.duration.toLong())
.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, musicData.albumArt).build()
}
fun getMusicMetadataList(): MutableList<MediaBrowserCompat.MediaItem> {
var index = 0
val metadataList = mutableListOf<MediaBrowserCompat.MediaItem>()
for ((_, musicData) in musicMap) {
val metadata = MediaMetadataCompat.Builder().putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, musicData.musicId)
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, musicData.title)
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, musicData.artist)
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, musicData.album)
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, musicData.duration.toLong())
.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, musicData.albumArt).build()
metadataList.add(MediaBrowserCompat.MediaItem(metadata.description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE))
index++
}
return metadataList
}
fun getArtwork(musicData: MusicData?, _albumName: String? = null, _albumId: Long? = null): Bitmap {
val musicId = musicData?.musicId
val albumName = musicData?.album ?: _albumName
val albumId = musicData?.albumId ?: _albumId
if (albumName == null || albumId == null) return defaultArtwork
try {
val albumArtFile = File(context.filesDir, "${musicData?.albumId}.bmp")
val albumArtUri = Uri.parse("content://media/external/audio/albumart")
val albumUri = ContentUris.withAppendedId(albumArtUri, albumId)
if (albumArtFile.exists()) {
BitmapFactory.decodeFile(albumArtFile.absolutePath)?.let {
artworkMap[albumName] = it
musicMap[musicId]?.albumArt = it
return it
}
}
val inputStream = context.contentResolver.openInputStream(albumUri)
BitmapFactory.decodeStream(inputStream)?.let {
artworkMap[albumName] = it
musicMap[musicId]?.albumArt = it
return it
}
} catch (e: FileNotFoundException) {
} catch (e: IOException) {
} catch (e: Exception) {
e.printStackTrace()
}
if (musicMap[musicId]?.albumArt != null) return musicMap[musicId]?.albumArt!!
if (artworkMap[albumName] != null) return artworkMap[albumName]!!
val char1 = if (albumName.isNotEmpty()) albumName[0].toString().toUpperCase(Locale.ROOT) else null
val char2 = if (albumName.length > 1) albumName[1].toString().toUpperCase(Locale.ROOT) else null
val resolution = settings.pGetString("pResolution", "400")?.toInt() ?: 400
val defaultArtworkView = when (resolution) {
800 -> View.inflate(context, R.layout.view_default_artwork_0, null)
600 -> View.inflate(context, R.layout.view_default_artwork_1, null)
400 -> View.inflate(context, R.layout.view_default_artwork_2, null)
300 -> View.inflate(context, R.layout.view_default_artwork_3, null)
200 -> View.inflate(context, R.layout.view_default_artwork_4, null)
else -> View.inflate(context, R.layout.view_default_artwork_2, null)
}
if (char1 != null && char2 != null) {
defaultArtworkView.findViewById<TextView>(R.id.VDA_Char1).text = char1
defaultArtworkView.findViewById<TextView>(R.id.VDA_Char2).text = char2
} else if (char1 != null && char2 == null) {
defaultArtworkView.findViewById<TextView>(R.id.VDA_Char1).text = char1
defaultArtworkView.findViewById<TextView>(R.id.VDA_Char2).text = char1
} else if (char1 == null && char2 == null) {
global.log(TAG, "char1 & char2 name is null so return default artwork")
return defaultArtwork
}
defaultArtworkView.findViewById<ConstraintLayout>(R.id.VDA_Background).background = when (global.getStringColorCode(albumName)) {
0 -> ColorDrawable(ContextCompat.getColor(context, R.color.colorDefaultArtwork0))
1 -> ColorDrawable(ContextCompat.getColor(context, R.color.colorDefaultArtwork1))
2 -> ColorDrawable(ContextCompat.getColor(context, R.color.colorDefaultArtwork2))
3 -> ColorDrawable(ContextCompat.getColor(context, R.color.colorDefaultArtwork3))
4 -> ColorDrawable(ContextCompat.getColor(context, R.color.colorDefaultArtwork4))
else -> ColorDrawable(ContextCompat.getColor(context, R.color.colorDefaultArtwork0))
}
if (defaultArtworkView.measuredHeight <= 0) {
defaultArtworkView.measure(
View.MeasureSpec.makeMeasureSpec(resolution, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(resolution, View.MeasureSpec.EXACTLY)
)
val defBitmap = Bitmap.createBitmap(defaultArtworkView.measuredWidth, defaultArtworkView.measuredHeight, Bitmap.Config.ARGB_8888)
val canvas = Canvas(defBitmap)
defaultArtworkView.layout(0, 0, defaultArtworkView.measuredWidth, defaultArtworkView.measuredHeight)
defaultArtworkView.draw(canvas)
val scaleX = (0.7f * defBitmap.width).toInt()
val scaleY = (0.7f * defBitmap.height).toInt()
val startX = (defBitmap.width - scaleX) / 2
val startY = (defBitmap.height - scaleY) / 2
val resultBitmap = Bitmap.createBitmap(defBitmap, startX, startY, scaleX, scaleY, null, true)
artworkMap[albumName] = resultBitmap
musicMap[musicId]?.albumArt = resultBitmap
return resultBitmap
}
return defaultArtwork
}
fun updateQueueIndex(musicData: MusicData?) {
for ((i, data) in queueItems.withIndex()) {
if (data.description.mediaId == musicData?.musicId) {
queueIndex = i
break
}
}
}
fun initQueueItems() {
val arrays = getQueueItem()
queueItems = arrays.first
queueItemsOriginal = arrays.second
if (queueItems.isEmpty() || queueItemsOriginal.isEmpty()) {
queueItems.clear()
for ((i, item) in getMusicMetadataList().withIndex()) {
queueItems.add(MediaSessionCompat.QueueItem(item.description, i.toLong()))
}
queueItemsOriginal.addAll(queueItems)
}
}
private fun saveQueueItem() {
thread {
val dataList = mutableListOf<String>()
val dataList2 = mutableListOf<String>()
val queueItems = queueItems.toList()
val queueItemsOriginal = queueItemsOriginal.toList()
queueItems.forEach { dataList.add(it.description.mediaId ?: "") }
queueItemsOriginal.forEach { dataList2.add(it.description.mediaId ?: "") }
val json = Gson().toJson(dataList)
val json2 = Gson().toJson(dataList2)
settings.setString(SETTING_MUSIC_VALUE, "queueItems", json)
settings.setString(SETTING_MUSIC_VALUE, "queueItemsOriginal", json2)
}
}
fun addQueueItemsList(musicList: List<MusicData>) {
val addList = mutableListOf<MediaSessionCompat.QueueItem>()
for ((addCount, data) in musicList.withIndex()) {
addList.add(MediaSessionCompat.QueueItem(getMusicMediaItem(data).description, (queueItems.size + addCount).toLong()))
}
queueItems.addAll(if (settings.shuffleMode) addList.shuffled() else addList)
queueItemsOriginal.addAll(addList)
}
fun setQueueItemList(newMusicData: MusicData?, musicList: List<MusicData>, onShuffle: Boolean = true) {
val tempList = mutableListOf<MediaSessionCompat.QueueItem>()
for ((i, musicData) in musicList.withIndex()) {
tempList.add(MediaSessionCompat.QueueItem(getMusicMediaItem(musicData).description, i.toLong()))
}
queueItems.clear()
queueItems.addAll(tempList)
queueItemsOriginal.clear()
queueItemsOriginal.addAll(queueItems)
updateQueueIndex(newMusicData)
if (settings.shuffleMode && onShuffle) {
onShuffle(PlaybackStateCompat.SHUFFLE_MODE_ALL)
}
}
private fun getQueueItem(): Pair<MutableList<MediaSessionCompat.QueueItem>, MutableList<MediaSessionCompat.QueueItem>> {
val json = settings.getString(SETTING_MUSIC_VALUE, "queueItems", null) ?: return Pair(mutableListOf(), mutableListOf())
val json2 = settings.getString(SETTING_MUSIC_VALUE, "queueItemsOriginal", null) ?: return Pair(mutableListOf(), mutableListOf())
val arrayList: List<String> = Gson().fromJson(json, ArrayList<String>().javaClass) ?: listOf()
val arrayList2: List<String> = Gson().fromJson(json2, ArrayList<String>().javaClass) ?: listOf()
val items = mutableListOf<MediaSessionCompat.QueueItem>()
val items2 = mutableListOf<MediaSessionCompat.QueueItem>()
val item3 = mutableListOf<MusicData?>()
for ((i, id) in arrayList.withIndex()) {
val description = getMusicMediaItem(null, id).description
items.add(MediaSessionCompat.QueueItem(description, i.toLong()))
item3.add(musicMap[id])
}
for ((i, id) in arrayList2.withIndex()) {
val description = getMusicMediaItem(null, id).description
items2.add(MediaSessionCompat.QueueItem(description, i.toLong()))
}
return Pair(items, items2)
}
fun setMusicMetadata(
musicData: MusicData,
newTitle: String,
newArtist: String,
newAlbum: String,
newTrack: String,
newYear: String,
newAlbumId: String,
newAlbumArtUri: Uri?
): Boolean {
musicData.uri ?: return false
val fileName = global.getFileName(musicData.path) ?: return false
val artworkFileName = "${musicData.albumId}.bmp"
val inputStream = context.contentResolver.openInputStream(musicData.uri!!) ?: return false
val inputStream2 = newAlbumArtUri?.let { context.contentResolver.openInputStream(it) }
context.openFileOutput(fileName, Context.MODE_PRIVATE).use { outputStream ->
inputStream.use { it.copyTo(outputStream) }
}
context.openFileOutput(artworkFileName, Context.MODE_PRIVATE).use { outputStream ->
inputStream2?.use { it.copyTo(outputStream) }
}
val albumArtUri = Uri.parse("content://media/external/audio/albumart")
val albumUri = ContentUris.withAppendedId(albumArtUri, musicData.albumId)
context.contentResolver.delete(albumUri, null, null)
val artwork = ArtworkFactory.createArtworkFromFile(File(context.filesDir, artworkFileName))
val audioFile = AudioFileIO.read(File(context.filesDir, fileName))
audioFile.tag.apply {
setField(FieldKey.TITLE, newTitle)
setField(FieldKey.ARTIST, newArtist)
setField(FieldKey.ALBUM, newAlbum)
setField(FieldKey.TRACK, newTrack)
setField(FieldKey.YEAR, newYear)
newAlbumArtUri?.let { setField(artwork) }
}
audioFile.commit()
val localFile = File(context.filesDir, fileName)
if (localFile.exists()) {
context.contentResolver.openOutputStream(musicData.uri!!)?.use { outputStream ->
FileInputStream(localFile).use { it.copyTo(outputStream) }
}
}
localFile.delete()
val contentValues = ContentValues().apply {
put(MediaStore.Audio.Media.ALBUM_ID, newAlbumId.toInt())
}
context.contentResolver.update(musicData.uri!!, contentValues, null, null)
clearData()
getMusic()
return true
}
fun createMusicNotify(notificationManager: NotificationManager, notifyId: Int): Notification? {
val musicData = currentMusicData ?: return null
val notifyTitle = musicData.title
val notifyText = musicData.artist
val channelName = context.getString(R.string.mediaNotifyChannel)
val channelDescription = context.getString(R.string.mediaNotifyDescription)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && notificationManager.getNotificationChannel(notifyId.toString()) == null) {
val channel = NotificationChannel(notifyId.toString(), channelName, NotificationManager.IMPORTANCE_LOW).apply {
description = channelDescription
}
notificationManager.createNotificationChannel(channel)
}
val stackBuilder = TaskStackBuilder.create(context).addNextIntent(Intent(context, MusicActivity::class.java))
val pendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
val notificationBuilder = NotificationCompat.Builder(context, notifyId.toString()).apply {
setStyle(
androidx.media.app.NotificationCompat.MediaStyle().setMediaSession(mediaSession.sessionToken).setShowActionsInCompactView(0, 1, 2)
.setShowCancelButton(true)
.setCancelButtonIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_STOP))
)
setSmallIcon(R.drawable.ic_music)
setLargeIcon(getArtwork(musicData))
setContentTitle(notifyTitle)
setContentText(notifyText)
setAutoCancel(false)
setDeleteIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_STOP))
setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
setContentIntent(pendingIntent)
}
var action = NotificationCompat.Action(
R.drawable.notification_backward_48,
context.getString(R.string.SkipToPrevious),
MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS)
)
notificationBuilder.addAction(action)
action = if (currentMusicState == PlaybackStateCompat.STATE_PLAYING) {
NotificationCompat.Action(
R.drawable.notification_pause_48,
context.getString(R.string.pause),
MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_PAUSE)
)
} else {
NotificationCompat.Action(
R.drawable.notification_play_48,
context.getString(R.string.play),
MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_PLAY)
)
}
notificationBuilder.addAction(action)
action = NotificationCompat.Action(
R.drawable.notification_forward_48,
context.getString(R.string.SkipToNext),
MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_SKIP_TO_NEXT)
)
notificationBuilder.addAction(action)
return notificationBuilder.build()
}
abstract class MusicCallback {
open fun onMusicPlayerConnected() {}
open fun onMusicPlayerDisconnected() {}
open fun onUpdatePlayerState(state: Int) {}
open fun onUpdatePlayerVolume(volumeLeft: Float, volumeRight: Float) {}
open fun onUpdatePlayerBass(strength: Short) {}
open fun onUpdatePlayerEqualizer(hzPair: Pair<Int, Int>) {}
open fun onUpdatePlayerReverbEffect(effect: Short) {}
open fun onUpdateCurrentMusicData(newData: MusicData) {}
open fun onUpdateMusicMetadata(metadata: MediaMetadataCompat) {}
open fun onUpdateMusicProgress(progress: Long) {}
}
interface MusicListener {
fun onSetMusicData(musicData: MusicData)
fun onPlayFromMusicData(musicData: MusicData, focedFirst: Boolean = false)
fun onPlay()
fun onPause()
fun onStop()
fun onSkipToNext(musicData: MusicData, isPlay: Boolean)
fun onSkipToPrevious(musicData: MusicData, isPlay: Boolean)
fun onSeekTo(progress: Long)
}
interface OnCurrentMusicChanged {
fun onCurrentMusicChanged(newData: MusicData)
}
companion object {
const val MUSIC_TAG = "<MUSIC>"
}
}
data class MusicData(
val musicId: String,
val path: String,
var albumArt: Bitmap?,
val title: String,
val artist: String,
val album: String,
val albumId: Long,
val duration: Int,
val truck: String?,
val year: String,
var favorite: Boolean?,
var uri: Uri? = null,
var queueId: Int = -1
)
###その4
MusicClassを超省略して、主要な関数のみ説明。
#####MusicClassの基本
インスタンスはアプリ内で一つだけ作成。トップレベルで保持することを推奨。(今回はトップレベルで"music"という名でインスタンスを作成している)
ApplicationクラスのonCreateなどで作成することを推奨。
#####initialize
MusicClassを初期化&音楽ライブラリの準備&MediaBrowserService接続準備
#####connect, disconnect
MediaBrowserと接続or切断するための関数。
#####attach. detach
MediaBrowserからクラスに接続or切断するための関数。
上記にも書いたが、基本は全ての音楽関連処理をこのクラスで行う。実際にMediaPlayerなどを使って音楽を再生するのはMusieServiceの役目。
#####onPlay, onPause, onStop, etc...
音楽を再生、中断、中止など。
#####getMusic
contentProviderから音楽情報を引っ張ってくる。MediaBrowserに頼らない私流のやり方。外部から安易に呼び出さないこと推奨。
#####getArtwork
artworkを取得。今気づいたけど、オリジナルの処理が入っているからエラー出るかも。ごめん。ここは重要ではないので消しても構わない。
#####MusicCallback
MusicClassの状態変化によって呼び出される。機能は関数名そのまま。
#####MusiscListener
音楽の再生状態の変化によって呼び出される。MediaPlayerなどの音楽を実際に再生するクラス以外overrideしてはいけない。
#####reserveMusicAction
MediaBrowserとまだ接続してないけど、接続したらやってね!という動作を登録しておきたいときに使用する。
#####setCurrentMusic
currentMusicを更新したいときに呼ぶ。MediaPlayerなどの音楽を実際に再生するクラス以外overrideしてはいけない。
###その5
以下MusicServiceのソースコード。これをコピーして貼り付け。
(自分のアプリからそのまま引っ張ってきているのでエラー多いかも。使わないなと思ったところは消して構わない。気にすることなかれ。)
class MusicService : MediaBrowserServiceCompat() {
private lateinit var audioFocusRequest: AudioFocusRequestCompat
private lateinit var bassBoost: BassBoost
private lateinit var equalizer: Equalizer
private lateinit var presetReverb: PresetReverb
private var musicPlayer = MediaPlayer()
private val audioManager by lazy { getSystemService(Context.AUDIO_SERVICE) as AudioManager }
private val audioNoisyFilter = IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
private var bootFlag = true
private val audioNoisyReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
music.onPause()
}
}
private val completionListener = MediaPlayer.OnCompletionListener {
global.log(MUSIC_TAG, "MusicService onCompletionListener: onComplete")
when (settings.repeatMode) {
PlaybackStateCompat.REPEAT_MODE_ALL -> {
music.onSkipToNext()
}
PlaybackStateCompat.REPEAT_MODE_ONE -> {
music.onSeek(0)
music.getCurrentMusic()?.let { it1 -> music.onPlay(it1, null, onShuffle = true) }
}
PlaybackStateCompat.REPEAT_MODE_NONE -> {
if (music.getQueueIndex() + 1 >= music.getQueueList().size) music.onPause()
music.onSkipToNext()
}
else -> music.onSkipToNext()
}
}
private val playerErrorListener = MediaPlayer.OnErrorListener { _, _, _ ->
Log.d(MUSIC_TAG, "service: Invalid state of music player.")
resetMusicPlayer()
return@OnErrorListener true
}
private val audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { audioFocus ->
when (audioFocus) {
AudioManager.AUDIOFOCUS_GAIN -> {
global.log(MUSIC_TAG, "MusicService : AudioFocus gain.")
music.getCurrentMusic()?.let { music.onPlay(it, null) }
}
AudioManager.AUDIOFOCUS_LOSS -> {
global.log(MUSIC_TAG, "MusicService : AudioFocus lost.")
music.onPause()
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
global.log(MUSIC_TAG, "MusicService : AudioFocus loss transient.")
music.onPause()
}
}
}
private val musicCallback = object : MusicClass.MusicCallback() {
override fun onMusicPlayerConnected() {
prepare()
}
override fun onUpdatePlayerState(state: Int) {
musicNotify()
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) music.getCurrentVolumeDb()
}
override fun onUpdatePlayerVolume(volumeLeft: Float, volumeRight: Float) {
musicPlayer.setVolume(volumeLeft, volumeRight)
Log.d(MUSIC_TAG, "set music volume ($volumeLeft, $volumeRight).")
}
override fun onUpdatePlayerBass(strength: Short) {
try {
bassBoost.setStrength(strength)
Log.d(MUSIC_TAG, "bass boost. STG: $strength")
} catch (e: Exception) {
global.stackTrace(e.toString())
}
}
override fun onUpdatePlayerEqualizer(hzPair: Pair<Int, Int>) {
try {
equalizer.setBandLevel(hzPair.first.toShort(), hzPair.second.toShort())
Log.d(MUSIC_TAG, "set equalizer. $hzPair")
} catch (e: Exception) {
global.stackTrace(e.toString())
}
}
override fun onUpdatePlayerReverbEffect(effect: Short) {
try {
presetReverb.preset = effect
Log.d(MUSIC_TAG, "set reverb effect. $effect")
} catch (e: Exception) {
global.stackTrace(e.toString())
}
}
}
private val musicListener = object : MusicClass.MusicListener {
override fun onSetMusicData(musicData: MusicData) {
global.log(MUSIC_TAG, "MusicService onSetMusicData: Set new music data.")
musicPlayer.reset()
musicPlayer.setDataSource(musicData.path)
musicPlayer.prepare()
global.addMusicHistory(musicData.musicId)
musicNotify()
}
override fun onPlayFromMusicData(musicData: MusicData, focedFirst: Boolean) {
onSetMusicData(musicData)
if (musicData.musicId == music.getCurrentMusic()?.musicId && settings.pGetBoolean("pMiddlePlay", true) && !focedFirst) {
onSeekTo(settings.currentMusicProgress)
}
music.setCurrentMusic(musicData)
music.onPlay(musicData, null)
}
override fun onPlay() {
if (gainAudioFocus()) {
val baseAudioVolume = settings.pGetInt("qBaseVolume", 5).toFloat() / 10f
val volume = if (settings.pGetBoolean("qDynamicNormalizer", false)) {
music.dynamicNormalize(music.getCurrentMusic()) ?: baseAudioVolume
} else baseAudioVolume
Log.d(MUSIC_TAG, "set music volume $volume.")
registerReceiver(audioNoisyReceiver, audioNoisyFilter)
music.setVolume(volume, volume)
musicPlayer.setOnCompletionListener(completionListener)
musicPlayer.start()
global.setTimer()
}
}
override fun onPause() {
musicPlayer.pause()
releaseAudioFocus()
removeReceiver()
global.cancelTimer()
}
override fun onStop() {
musicPlayer.stop()
releaseAudioFocus()
removeReceiver()
global.cancelTimer()
disorganize()
}
override fun onSkipToNext(musicData: MusicData, isPlay: Boolean) {
onSetMusicData(musicData)
if (isPlay) onPlayFromMusicData(musicData, true)
else {
music.setCurrentMusic(musicData)
music.progressUpdate(0)
}
}
override fun onSkipToPrevious(musicData: MusicData, isPlay: Boolean) {
onSetMusicData(musicData)
if (isPlay) onPlayFromMusicData(musicData, true)
else {
music.setCurrentMusic(musicData)
music.progressUpdate(0)
}
}
override fun onSeekTo(progress: Long) {
musicPlayer.seekTo(progress.toInt())
music.progressUpdate(progress)
}
private fun removeReceiver() {
try {
unregisterReceiver(audioNoisyReceiver)
} catch (e: IllegalArgumentException) {
} catch (e: Exception) {
e.printStackTrace()
}
}
}
private val progressUpdate = object : Runnable {
override fun run() {
if (musicPlayer.isPlaying) music.progressUpdate(musicPlayer.currentPosition.toLong())
global.handler.postDelayed(this, 500)
}
}
override fun onGetRoot(clientPackageName: String, clientUid: Int, rootHints: Bundle?): BrowserRoot? {
global.log(MUSIC_TAG, "MusicService onGetRoot")
return BrowserRoot("All-OK", null)
}
override fun onLoadChildren(parentId: String, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
global.log(MUSIC_TAG, "MusicService onLoadChildren")
result.sendResult(music.getMusicMetadataList())
}
override fun onCreate() {
super.onCreate()
music.addCallback(musicCallback)
music.setListener(musicListener)
music.attach(this, musicPlayer)
global.handler.postDelayed(progressUpdate, 500)
global.log(MUSIC_TAG, "MusicService onCreate: connected to Server.")
}
override fun onDestroy() {
super.onDestroy()
global.log(MUSIC_TAG, "MusicService onDestroy: disconnected from server.")
}
private fun prepare() {
global.log(MUSIC_TAG, "MusicService prepare: preparing...")
resetMusicPlayer()
initAudioEffect()
audioFocusRequest = AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN).setAudioAttributes(
AudioAttributesCompat.Builder().setUsage(AudioAttributesCompat.USAGE_MEDIA).setContentType(AudioAttributesCompat.CONTENT_TYPE_MUSIC)
.build()
).setOnAudioFocusChangeListener(audioFocusChangeListener).build()
music.getCurrentMusic()?.let { musicListener.onSetMusicData(it) }
global.log(MUSIC_TAG, "MusicService prepare: prepared.")
}
private fun disorganize() {
global.log(MUSIC_TAG, "MusicService disorganize: disorganizing...")
bootFlag = false
stopForeground(false)
musicPlayer.release()
bassBoost.release()
equalizer.release()
global.handler.removeCallbacks(progressUpdate)
music.detach()
music.removeListener()
music.removeCallback(musicCallback)
music.disconnect()
stopSelf()
global.log(MUSIC_TAG, "MusicService disorganize: disorganized.")
}
private fun initAudioEffect() {
bassBoost = BassBoost(100, musicPlayer.audioSessionId)
equalizer = Equalizer(100, musicPlayer.audioSessionId)
presetReverb = PresetReverb(100, 0)
musicPlayer.attachAuxEffect(presetReverb.id)
musicPlayer.setAuxEffectSendLevel(1f)
bassBoost.enabled = false
equalizer.enabled = false
presetReverb.enabled = false
if (settings.pGetBoolean("qBassBoost", false)) {
music.setBass(settings.pGetInt("qBassBoostValue", 0))
}
music.setReverbEffect(settings.pGetString("qReverbEffector", "0")?.toShort() ?: 0)
val bands = equalizer.numberOfBands
global.minEQLevel = equalizer.bandLevelRange[0]
global.maxWQLevel = equalizer.bandLevelRange[1]
global.equalizerBandMap.clear()
for (i in 0 until bands) {
global.equalizerBandMap[i] = (equalizer.getCenterFreq(i.toShort()) / 1000).toString() + "Hz"
}
AudioEffect.queryEffects().forEach {
Log.d(MUSIC_TAG, "AUDIO EFFECT: ${it.name}, TYPE: ${it.type}")
}
bassBoost.enabled = true
equalizer.enabled = true
presetReverb.enabled = true
Log.d(MUSIC_TAG, "audio effect initialized.")
}
private fun resetMusicPlayer() {
musicPlayer.release()
musicPlayer = MediaPlayer()
musicPlayer.setOnErrorListener(playerErrorListener)
}
private fun gainAudioFocus(): Boolean {
return when (AudioManagerCompat.requestAudioFocus(audioManager, audioFocusRequest)) {
AudioManager.AUDIOFOCUS_REQUEST_GRANTED -> true
AudioManager.AUDIOFOCUS_REQUEST_FAILED -> false
AudioManager.AUDIOFOCUS_REQUEST_DELAYED -> false
else -> false
}
}
private fun releaseAudioFocus() {
AudioManagerCompat.abandonAudioFocusRequest(audioManager, audioFocusRequest)
}
private fun musicNotify() {
if (!bootFlag) return
try {
val notificationManager = baseContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notify = music.createMusicNotify(notificationManager, MUSIC_NOTIFY_ID) ?: return
if (music.getCurrentMusicState() == PlaybackStateCompat.STATE_PLAYING) startForeground(MUSIC_NOTIFY_ID, notify)
else stopForeground(false)
notificationManager.notify(MUSIC_NOTIFY_ID, notify)
} catch (e: Exception) {
global.stackTrace(e.toString())
}
}
companion object {
const val MUSIC_NOTIFY_ID = 671
}
}
###その6
MusicServiceを超省略して、主要な関数のみ説明。
#####prepare
MusicServiceを初期化&MusicClassとの通信確立&再生準備
#####disorganize
MusicServiceを破棄&MusicClassとの通信を切断
#####musicNotify
音楽再生中はForegroundServiceとして振る舞い、中断中はいつでも破棄可能とする。
#####musicCallback, musicListener
MusicClassからの指示を拾い、実際に音楽を再生したりする。
#####progressUpdate
0.5sずつに呼び出して音楽の再生状況をMusicClassに報告する。(MusicClassはそれを受けて音楽の再生状況をActivityに伝えたりする)
###その7
あとはApplicationクラスでMusicClassをインスタンス化して、実際にActivityなどでmusic.onPlayを呼び出せば音楽が再生される。
##まとめ
質問を随時受付中。
この記事は自分のために高速で作ったものなので不備ありありだが、質問されれれば全力で答えていく。