20200405のAndroidに関する記事は10件です。

MediaSessionを使って秒で音楽アプリを作成する(超簡易版)

音楽アプリを作りたい

音楽アプリを作りたいんだけど、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を呼び出せば音楽が再生される。

まとめ

質問を随時受付中。
この記事は自分のために高速で作ったものなので不備ありありだが、質問されれれば全力で答えていく。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Delegated Properties で SharedPreferences を綺麗にしてみた

はじめに

Delegated Properties を使って SharedPreferences を綺麗にしてみました。
(タイトルまんまですが..)

SharedPreferencesを使うクラスで保存する値が増えるたびに類似したコードが量産されていたので、
リファクタリングのタイミングで導入してみての使い勝手とコードを残します。

参考にしたサイトは以下です。
公式
Kotlinアンチパターン slide 66~
KotlinのReadWritePropertyを使ってAndroidのPreferenceをナウく書く

コードとユニットテスト

修正前

このまま、別のプロパティが追加されると増えるか、private メソッドを作るかでした...

class SharedPreferencesWrapper(private val context: Context) {

    private var defaultPreferences = PreferenceManager.getDefaultSharedPreferences(context)

    var booleanKey: Boolean
        get() = defaultPreferences.getBoolean("booleanKey", false)
        set(value) = defaultPreferences.edit().putBoolean("booleanKey", value).apply()

    var stringKey: String
        get() = defaultPreferences.getString("stringKey", null) ?: ""
        set(value) = defaultPreferences.edit().putString("stringKey", value).apply()

    var intKey: Int
        get() = defaultPreferences.getInt("intKey", 0)
        set(value) = defaultPreferences.edit().putInt("intKey", value).apply()

    var floatKey: Float
        get() = defaultPreferences.getFloat("floatKey", 0f)
        set(value) = defaultPreferences.edit().putFloat("floatKey", value).apply()

    var longKey: Long
        get() = defaultPreferences.getLong("longKey", 0L)
        set(value) = defaultPreferences.edit().putLong("longKey", value).apply()

    var stringSetKey: Set<String>
        get() = defaultPreferences.getStringSet("stringSetKey", null) ?: hashSetOf()
        set(value) = defaultPreferences.edit().putStringSet("stringSetKey", value).apply()

    var dateKey: String
        get() = defaultPreferences.getString("dateKey", "") ?: ""
        set(value) = defaultPreferences.edit().putString("dateKey", value).apply()

    var dataClassKey: Color?
        get() {
            val str = defaultPreferences.getString("dataClassKey", "")
            str?.let {
                return jacksonObjectMapper().readValue<Color>(it)
            }
            return null
        }
        set(value) {
            val str = jacksonObjectMapper().writeValueAsString(value)
            defaultPreferences.edit().putString("dataClassKey", str).apply()
        }

    var listKey: List<Color>?
        get() {
            val str = defaultPreferences.getString("listKey", "")
            str?.let {
                return jacksonObjectMapper().readValue(it)
            }
            return null
        }
        set(value) {
            val str = jacksonObjectMapper().writeValueAsString(value)
            defaultPreferences.edit().putString("listKey", str).apply()
        }

}
data class Color(var name: String, var rgb:String)

Delegated Properties で 修正後

プロパティ の getter / setter を一行で定義できたので、
プロパティが増えてもクラス全体が極端に大きくなることはなさそうです。
(MutableSetなど元から少し型が変わっていますが..)

data class や list 構造などの場合、どこまで汎用的にするかですが、
数が少なければそのままでも良さそうです。
(今回、ObjectMapperにはJacksonを使いましたが、Gsonなど他のObjectMapperでも大丈夫です。)

class SharedPreferencesWrapperByDelegate(private val context: Context) {

    private var defaultPreferences = PreferenceManager.getDefaultSharedPreferences(context)

    var booleanKey: Boolean by BooleanDelegate(defaultPreferences, "booleanKey", false)

    var stringKey: String by StringDelegate(defaultPreferences, "stringKey", "")

    var intKey: Int by IntDelegate(defaultPreferences, "intKey", 0)

    var floatKey: Float by FloatDelegate(defaultPreferences, "floatKey", 0F)

    var longKey: Long by LongDelegate(defaultPreferences, "longKey", 0L)

    var stringSetKey: MutableSet<String> by SetDelegate(defaultPreferences, "stringSetKey", hashSetOf())

    var dateKey: Date by DateDelegate(defaultPreferences, "stringSetKey", Date())

    var dataClassKey: Color? by ColorDelegate(defaultPreferences, "dataClassKey", null)

    var listKey: MutableList<Color>? by ListDelegate(defaultPreferences, "dataClassKey", null)
}

Delegated Properties のクラス

class BooleanDelegate(
    private val sp: SharedPreferences,
    private val name: String,
    private val defaultValue: Boolean
) : ReadWriteProperty<Any, Boolean> {
    override fun getValue(thisRef: Any, property: KProperty<*>) = sp.getBoolean(name, defaultValue)
    override fun setValue(thisRef: Any, property: KProperty<*>, value: Boolean) =
        sp.edit().putBoolean(name, value).apply()
}

class StringDelegate(
    private val sp: SharedPreferences,
    private val name: String,
    private val defaultValue: String
) : ReadWriteProperty<Any, String> {
    override fun getValue(thisRef: Any, property: KProperty<*>): String =
        sp.getString(name, defaultValue) ?: ""

    override fun setValue(thisRef: Any, property: KProperty<*>, value: String) =
        sp.edit().putString(name, value).apply()
}

class IntDelegate(
    private val sp: SharedPreferences,
    private val name: String,
    private val defaultValue: Int
) : ReadWriteProperty<Any, Int> {
    override fun getValue(thisRef: Any, property: KProperty<*>) = sp.getInt(name, defaultValue)
    override fun setValue(thisRef: Any, property: KProperty<*>, value: Int) =
        sp.edit().putInt(name, value).apply()
}

class FloatDelegate(
    private val sp: SharedPreferences,
    private val name: String,
    private val defaultValue: Float
) : ReadWriteProperty<Any, Float> {
    override fun getValue(thisRef: Any, property: KProperty<*>) = sp.getFloat(name, defaultValue)
    override fun setValue(thisRef: Any, property: KProperty<*>, value: Float) =
        sp.edit().putFloat(name, value).apply()
}

class LongDelegate(
    private val sp: SharedPreferences,
    private val name: String,
    private val defaultValue: Long
) : ReadWriteProperty<Any, Long> {
    override fun getValue(thisRef: Any, property: KProperty<*>) = sp.getLong(name, defaultValue)
    override fun setValue(thisRef: Any, property: KProperty<*>, value: Long) =
        sp.edit().putLong(name, value).apply()
}

class SetDelegate(
    private val sp: SharedPreferences,
    private val name: String,
    private val defaultValue: MutableSet<String>
) : ReadWriteProperty<Any, MutableSet<String>> {
    override fun getValue(thisRef: Any, property: KProperty<*>): MutableSet<String> =
        sp.getStringSet(name, defaultValue) ?: hashSetOf()

    override fun setValue(thisRef: Any, property: KProperty<*>, value: MutableSet<String>) =
        sp.edit().putStringSet(name, value).apply()
}

class DateDelegate(
    private val sp: SharedPreferences,
    private val name: String,
    private val defaultValue: Date
) : ReadWriteProperty<Any, Date> {
    override fun getValue(thisRef: Any, property: KProperty<*>) = Date(sp.getLong(name, 0L))
    override fun setValue(thisRef: Any, property: KProperty<*>, value: Date) =
        sp.edit().putLong(name, value.time).apply()
}

class ColorDelegate(
    private val sp: SharedPreferences,
    private val name: String,
    private val defaultValue: Color?
) : ReadWriteProperty<Any, Color?> {
    override fun getValue(thisRef: Any, property: KProperty<*>): Color? {
        val str = sp.getString(name, "") ?: ""
        return try {
            jacksonObjectMapper().readValue<Color>(str)
        } catch (e: Exception) {
            defaultValue
        }
    }

    override fun setValue(thisRef: Any, property: KProperty<*>, value: Color?) {
        val str = jacksonObjectMapper().writeValueAsString(value)
        sp.edit().putString(name, str).apply()
    }
}

class ListDelegate(
    private val sp: SharedPreferences,
    private val name: String,
    private val defaultValue: MutableList<Color>?
) : ReadWriteProperty<Any, MutableList<Color>?> {
    override fun getValue(thisRef: Any, property: KProperty<*>): MutableList<Color>? {
        val str = sp.getString(name, "") ?: ""
        return try {
            jacksonObjectMapper().readValue<MutableList<Color>>(str)
        } catch (e: Exception) {
            defaultValue
        }
    }

    override fun setValue(thisRef: Any, property: KProperty<*>, value: MutableList<Color>?) {
        val str = jacksonObjectMapper().writeValueAsString(value)
        sp.edit().putString(name, str).apply()
    }
}

ユニットテスト

コード自体に対するユニットテストも書きました。
初期化、デフォルト値の検証をして、コードレベルの問題がないことを確認しています。
SharedPreferences をユニットテスト用にクラスを作って、
Contextがなくてもテスト対象のコードが実行出来るようにしています。

class DelegatesUnitTest {

    @Test
    fun test_BooleanDelegate() {
        val wrapper = UnitTestSharedPreferencesWrapper()
        assertEquals(wrapper.booleanKey, false)
        wrapper.booleanKey = true
        assertEquals(wrapper.booleanKey, true)
    }

    @Test
    fun test_StringDelegate() {
        val wrapper = UnitTestSharedPreferencesWrapper()
        assertEquals(wrapper.stringKey, "")
        wrapper.stringKey = "string"
        assertEquals(wrapper.stringKey, "string")
    }

    @Test
    fun test_IntDelegate() {
        val wrapper = UnitTestSharedPreferencesWrapper()
        assertEquals(wrapper.intKey, 0)
        wrapper.intKey = 100
        assertEquals(wrapper.intKey, 100)
    }

    @Test
    fun test_FloatDelegate() {
        val wrapper = UnitTestSharedPreferencesWrapper()
        assertEquals(wrapper.floatKey, 0f)
        wrapper.floatKey = 200f
        assertEquals(wrapper.floatKey, 200f)
    }

    @Test
    fun test_LongDelegate() {
        val wrapper = UnitTestSharedPreferencesWrapper()
        assertEquals(wrapper.longKey, 0L)
        wrapper.longKey = 300L
        assertEquals(wrapper.longKey, 300L)
    }

    @Test
    fun test_StringSetDelegate() {
        val wrapper = UnitTestSharedPreferencesWrapper()
        assertTrue(wrapper.stringSetKey.isEmpty())
        val set = hashSetOf("one", "two")
        wrapper.stringSetKey = set
        assertTrue(wrapper.stringSetKey.size == 2)
        assertTrue(wrapper.stringSetKey.contains("one"))
        assertTrue(wrapper.stringSetKey.contains("two"))
    }

    @Test
    fun test_DateDelegate() {
        val wrapper = UnitTestSharedPreferencesWrapper()
        assertEquals(wrapper.dateKey, Date(0))
        val current = Date()
        wrapper.dateKey = current
        assertEquals(wrapper.dateKey, current)
    }

    @Test
    fun test_ObjectDelegate() {
        val wrapper = UnitTestSharedPreferencesWrapper()
        assertNull(wrapper.dataClassKey)
        wrapper.dataClassKey = Color("red", "ff0000")
        assertEquals(wrapper.dataClassKey, Color("red", "ff0000"))
    }

    @Test
    fun test_ListObjectDelegate() {
        val wrapper = UnitTestSharedPreferencesWrapper()
        assertNull(wrapper.listKey)
        val list = arrayListOf<Color>(Color("red", "ff0000"), Color("green", "00ff00"))
        wrapper.listKey = list
        assertTrue(wrapper.listKey!!.size == 2)
        assertTrue(wrapper.listKey!!.contains(Color("red", "ff0000")))
        assertTrue(wrapper.listKey!!.contains(Color("green", "00ff00")))
    }
}
class UnitTestSharedPreferencesWrapper() {

    private val data = hashMapOf<String?, Any?>()

    private var defaultPreferences = object : SharedPreferences {
        override fun contains(p0: String?): Boolean {
            TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
        }

        override fun getBoolean(p0: String?, p1: Boolean): Boolean {
            return data[p0] as? Boolean ?: p1
        }

        override fun unregisterOnSharedPreferenceChangeListener(p0: SharedPreferences.OnSharedPreferenceChangeListener?) {
            TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
        }

        override fun getInt(p0: String?, p1: Int): Int {
            return data[p0] as? Int ?: p1
        }

        override fun getAll(): MutableMap<String, *> {
            TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
        }

        override fun edit(): SharedPreferences.Editor {
            return object : SharedPreferences.Editor {
                override fun clear(): SharedPreferences.Editor {
                    TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
                }

                override fun putLong(p0: String?, p1: Long): SharedPreferences.Editor {
                    data[p0] = p1
                    return this
                }

                override fun putInt(p0: String?, p1: Int): SharedPreferences.Editor {
                    data[p0] = p1
                    return this
                }

                override fun remove(p0: String?): SharedPreferences.Editor {
                    TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
                }

                override fun putBoolean(p0: String?, p1: Boolean): SharedPreferences.Editor {
                    data[p0] = p1
                    return this
                }

                override fun putStringSet(
                    p0: String?,
                    p1: MutableSet<String>?
                ): SharedPreferences.Editor {
                    data[p0] = p1
                    return this
                }

                override fun commit(): Boolean {
                    TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
                }

                override fun putFloat(p0: String?, p1: Float): SharedPreferences.Editor {
                    data[p0] = p1
                    return this
                }

                override fun apply() {

                }

                override fun putString(p0: String?, p1: String?): SharedPreferences.Editor {
                    data[p0] = p1
                    return this
                }

            }
        }

        override fun getLong(p0: String?, p1: Long): Long {
            return data[p0] as? Long ?: p1
        }

        override fun getFloat(p0: String?, p1: Float): Float {
            return data[p0] as? Float ?: p1
        }

        override fun getStringSet(p0: String?, p1: MutableSet<String>?): MutableSet<String>? {
            return data[p0] as? MutableSet<String> ?: p1
        }

        override fun registerOnSharedPreferenceChangeListener(p0: SharedPreferences.OnSharedPreferenceChangeListener?) {
            TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
        }

        override fun getString(p0: String?, p1: String?): String? {
            return data[p0] as? String ?: p1
        }
    }

    var booleanKey: Boolean by BooleanDelegate(defaultPreferences, "booleanKey", false)

    var stringKey: String by StringDelegate(defaultPreferences, "stringKey", "")

    var intKey: Int by IntDelegate(defaultPreferences, "intKey", 0)

    var floatKey: Float by FloatDelegate(defaultPreferences, "floatKey", 0F)

    var longKey: Long by LongDelegate(defaultPreferences, "longKey", 0L)

    var stringSetKey: MutableSet<String> by SetDelegate(defaultPreferences, "stringSetKey", mutableSetOf())

    var dateKey: Date by DateDelegate(defaultPreferences, "dateKey", Date())

    var dataClassKey: Color? by ColorDelegate(defaultPreferences, "dataClassKey", null)

    var listKey: MutableList<Color>? by ListDelegate(defaultPreferences, "dataListClassKey", null)
}

まとめ

Delegated Properties は 考えて使えると、定型的な処理をかなり綺麗に依存できるかなと思いました。
ただ、ある程度使い方を限定しないとオレオレFWの世界に突入しそうでしたが...

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

flutter run で起きたエラー(No supported devices connected.)

はじめに

作成したアプリを実行するためにターミナルにて以下を実行したところ、
エラーが発生したので原因を解説します。

flutter run

エラー内容

エラー内容としてはこちらになります。

A new version of Flutter is available!
To update to the latest version, run "flutter upgrade".
No supported devices connected.

実際のターミナル画面では以下のように表示されています。
スクリーンショット 2020-04-05 19.51.21.png

エラーの原因

1.結論

結論から申し上げますと、
Flutterの最新バージョンにアップデートが必要ということです。

2.解説

では以下解説していきます。
まずエラー文に何が書いてあるのか翻訳してみましょう。

A new version of Flutter is available!
Flutterの新しいバージョンが利用可能です!

To update to the latest version, run "flutter upgrade".
最新バージョンに更新するには、「flutter upgrade」を実行してください。

No supported devices connected.
サポートされているデバイスが接続されていません。

となります。
翻訳そのままですね。
エラー文2行目のflutter upgradeをターミナルにて実行しましょう。

flutter upgrade

するとアップグレードが始まり、最後に以下が表示されます。
スクリーンショット 2020-04-05 20.10.34.png

確認ができたら、もう一度flutter runを実行すると無事にアプリが起動します。

flutter run

最後に

解説は以上となります。
最後までご覧くださりありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Flutter環境構築 - 2(iOSシミュレーター実行編)

はじめに

前回の「Flutter SDKインストール編」に引き続き、
今回は「iOSシミュレーター実行編」です!
Flutter SDKのインストール・Flutterコマンドのセットアップがお済みでない方は、
先に「Flutter SDKインストール編」をご覧ください。

「iOSシミュレーター実行編」でやること

本編では以下の項目を行います。

  • Xcodeのインストール
  • iOSシミュレーターのセットアップ
  • Flutterアプリを作成し実行する

では次項目より実際に作業していきます。

Xcodeをインストールする

1.App StoreよりXcodeをインストール

App StoreにてXcodeを検索し、インストールします。

すでにインストールされている方はアップデートがないかを確認し、
最新バージョンでなければアップデートする方が吉だと思います。
※ Flutter公式サイトより引用 ※
[ 最新の安定バージョンより古いバージョンでも動作する可能性がありますが、Flutter開発にはお勧めできません。古いバージョンのXcodeを使用してビットコードをターゲットにすることはサポートされておらず、機能しない可能性があります。 ]
スクリーンショット 2020-03-30 8.43.19.png

2.最新Ver.のXcodeを使用するようにXcodeコマンドラインツールを構成

ターミナルにて以下を実行します。
パスワードを求められるので入力します。

sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -runFirstLaunch

3.Xcodeライセンスの確認

以下を実行し内容を確認します。
spaceキーで下にスクロールすることができます。
qキーでページを抜けることができます。

sudo xcodebuild -license

ページを抜けた後、以下を入力することでライセンスに同意します。

agree

iOSシミュレーターのセットアップ

1.iOSシミュレーターを実行

ターミナルにて以下を実行します。

open -a Simulator

するとiOSシミュレーターが立ち上がります。
スクリーンショット 2020-04-05 16.51.22.png

Flutterアプリを作成し実行する

シンプルなFlutterアプリを作成し、実行していきます。

1.ディレクトリ作成

今回のサンプルアプリ用にディレクトリを作成します。
各々好きな名前で構いません。

mkdir 好きな名前

2.作成したディレクトリへ移動

cd 先ほど作成したディレクトリ名

3.Flutterアプリ作成

以下コマンドを入力すると、
先ほど作成したディレクトリの中に
my_appというサンプルのFlutterアプリが作成されます。

flutter create my_app

4.作成したmy_appへ移動

cd my_app

5.iOSシミュレーターでmy_appを実行

iOSシミュレーターを起動した状態で以下を実行します。

flutter run

するとiOSシミュレーター内でmy_appが実行されます。
右下の+ボタンを押すと数字がカウントアップされます。
ターミナルでqキーを入力するとアプリを終了できます。
スクリーンショット 2020-04-05 17.23.05.png
これでシンプルなFlutterアプリの作成・実行ができました!

最後に

以上で「iOSシミュレーター実行編」は終了となります。お疲れさまでした。
実機にてアプリの確認をしたい場合はFlutter公式サイト
「Deploy to iOS devices」欄を参考にしてみてください。
私はiPhoneユーザーではないので。(iPhone欲しい。。。)

最後までご覧くださりありがとうございました。

参考サイト[Flutter公式サイト]

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Flutter環境構築 - 2(Xcode・iOSシミュレーターセットアップ編)

はじめに

前回の「Flutter SDKインストール編」に引き続き、
今回は「Xcode・iOSシミュレーターセットアップ編」です!
Flutter SDKのインストール・Flutterコマンドのセットアップがお済みでない方は、
先に「Flutter SDKインストール編」をご覧ください。

「iOSシミュレーター実行編」でやること

本編では以下の項目を行います。

  • Xcodeのインストール
  • iOSシミュレーターのセットアップ
  • Flutterアプリを作成し実行する

では次項目より実際に作業していきます。

Xcodeをインストールする

1.App StoreよりXcodeをインストール

App StoreにてXcodeを検索し、インストールします。

すでにインストールされている方はアップデートがないかを確認し、
最新バージョンでなければアップデートする方が吉だと思います。
※ Flutter公式サイトより引用 ※
[ 最新の安定バージョンより古いバージョンでも動作する可能性がありますが、Flutter開発にはお勧めできません。古いバージョンのXcodeを使用してビットコードをターゲットにすることはサポートされておらず、機能しない可能性があります。 ]
スクリーンショット 2020-03-30 8.43.19.png

2.最新Ver.のXcodeを使用するようにXcodeコマンドラインツールを構成

ターミナルにて以下を実行します。
パスワードを求められるので入力します。

sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -runFirstLaunch

3.Xcodeライセンスの確認

以下を実行し内容を確認します。
spaceキーで下にスクロールすることができます。
qキーでページを抜けることができます。

sudo xcodebuild -license

ページを抜けた後、以下を入力することでライセンスに同意します。

agree

iOSシミュレーターのセットアップ

1.iOSシミュレーターを実行

ターミナルにて以下を実行します。

open -a Simulator

するとiOSシミュレーターが立ち上がります。
スクリーンショット 2020-04-05 16.51.22.png

Flutterアプリを作成し実行する

シンプルなFlutterアプリを作成し、実行していきます。

1.ディレクトリ作成

今回のサンプルアプリ用にディレクトリを作成します。
各々好きな名前で構いません。

mkdir 好きな名前

2.作成したディレクトリへ移動

cd 先ほど作成したディレクトリ名

3.Flutterアプリ作成

以下コマンドを入力すると、
先ほど作成したディレクトリの中に
my_appというサンプルのFlutterアプリが作成されます。

flutter create my_app

4.作成したmy_appへ移動

cd my_app

5.iOSシミュレーターでmy_appを実行

iOSシミュレーターを起動した状態で以下を実行します。

flutter run

するとiOSシミュレーター内でmy_appが実行されます。
右下の+ボタンを押すと数字がカウントアップされます。
ターミナルでqキーを入力するとアプリを終了できます。
スクリーンショット 2020-04-05 17.23.05.png
これでシンプルなFlutterアプリの作成・実行ができました!

最後に

以上で「Xcode・iOSシミュレーターセットアップ編」は終了となります。お疲れさまでした。
実機にてアプリの確認をしたい場合はFlutter公式サイト
「Deploy to iOS devices」欄を参考にしてみてください。
私はiPhoneユーザーではないので。(iPhone欲しい。。。)

最後までご覧くださりありがとうございました。

参考サイト[Flutter公式サイト]

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

mDNSでAndroidからRaspberryPiを探す

Raspberry Pi

avahi-daemonの設定

avahi-daemonのインストール(入っていなければ)

sudo apt-get install avahi-daemon

設定変更

sudo vi /etc/avahi/avahi-daemon.conf

noになっているとAndroid側で探索が出来なかった

avahi-daemon.conf
[publish]
publish-workstation=yes

avahi-daemon.service再起動

sudo systemctl restart avahi-daemon.service

host名確認

cat /etc/hostname
<Hostname>

Android

これをみた方が早いとは思う(参考)
https://developer.android.com/training/connect-devices-wirelessly/nsd.html

許可取り

AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET"/>

メンバー変数

    String TAG = "MainActivity";
    //確認したホスト名に変えておく
    String serviceName = "<Hostname>";

    private NsdManager nsdManager;
    private NsdManager.DiscoveryListener discoveryListener;
    private NsdManager.ResolveListener resolveListener;
    private NsdServiceInfo mService;

イニシャライズ

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        nsdManager = (NsdManager)(getApplicationContext().getSystemService(Context.NSD_SERVICE));
        initializeResolveListener();
        initializeDiscoveryListener();
        nsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryListener);
    }

関数定義

    public void initializeDiscoveryListener() {

        discoveryListener = new NsdManager.DiscoveryListener() {

            @Override
            public void onDiscoveryStarted(String regType) {
                Log.d(TAG, "Service discovery started");
            }

            @Override
            public void onServiceFound(NsdServiceInfo service) {
                //見つかるのが<hostname>.local[<macアドレス>]みたいな感じだったのでcontainsで対応
                if (service.getServiceName().contains(serviceName)) {
                    nsdManager.resolveService(service, resolveListener);
                    Log.d(TAG, "Same machine: " + serviceName);
                } 
            }

            @Override
            public void onServiceLost(NsdServiceInfo service) {
                Log.e(TAG, "service lost: " + service);
            }

            @Override
            public void onDiscoveryStopped(String serviceType) {
                Log.i(TAG, "Discovery stopped: " + serviceType);
            }

            @Override
            public void onStartDiscoveryFailed(String serviceType, int errorCode) {
                Log.e(TAG, "Discovery failed: Error code:" + errorCode);
                nsdManager.stopServiceDiscovery(this);
            }

            @Override
            public void onStopDiscoveryFailed(String serviceType, int errorCode) {
                Log.e(TAG, "Discovery failed: Error code:" + errorCode);
                nsdManager.stopServiceDiscovery(this);
            }
        };
    }


    public void initializeResolveListener() {
        resolveListener = new NsdManager.ResolveListener() {

            @Override
            public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
                // Called when the resolve fails. Use the error code to debug.
                Log.e(TAG, "Resolve failed: " + errorCode);
            }

            @Override
            public void onServiceResolved(NsdServiceInfo serviceInfo) {
                Log.e(TAG, "Resolve Succeeded. " + serviceInfo);

                if (serviceInfo.getServiceName().equals(serviceName)) {
                    Log.d(TAG, "Same IP.");
                    return;
                }
                mService = serviceInfo;
                int port = mService.getPort();
                InetAddress host = mService.getHost();
            }
        };
    }

全文

MainActivity.java
public class MainActivity extends AppCompatActivity {

    String TAG = "MainActivity";
    //確認したホスト名に変えておく
    String serviceName = "<Hostname>";

    private NsdManager nsdManager;
    private NsdManager.DiscoveryListener discoveryListener;
    private NsdManager.ResolveListener resolveListener;
    private NsdServiceInfo mService;

    // raspberry service type
    private static final String SERVICE_TYPE = "_workstation._tcp.";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        nsdManager = (NsdManager)(getApplicationContext().getSystemService(Context.NSD_SERVICE));
        initializeResolveListener();
        initializeDiscoveryListener();
        nsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryListener);
    }

    public void initializeDiscoveryListener() {

        discoveryListener = new NsdManager.DiscoveryListener() {

            @Override
            public void onDiscoveryStarted(String regType) {
                Log.d(TAG, "Service discovery started");
            }

            @Override
            public void onServiceFound(NsdServiceInfo service) {
                Log.d(TAG, "Service discovery success" + service);


                if (!service.getServiceType().equals(SERVICE_TYPE)) {
                    Log.d(TAG, "Unknown Service Type: " + service.getServiceType());
                } else if (service.getServiceName().equals(serviceName)) {
                    Log.d(TAG, "Same machine: " + serviceName);
                } else if (service.getServiceName().contains("NsdChat")){
                    nsdManager.resolveService(service, resolveListener);
                }
            }

            @Override
            public void onServiceLost(NsdServiceInfo service) {
                Log.e(TAG, "service lost: " + service);
            }

            @Override
            public void onDiscoveryStopped(String serviceType) {
                Log.i(TAG, "Discovery stopped: " + serviceType);
            }

            @Override
            public void onStartDiscoveryFailed(String serviceType, int errorCode) {
                Log.e(TAG, "Discovery failed: Error code:" + errorCode);
                nsdManager.stopServiceDiscovery(this);
            }

            @Override
            public void onStopDiscoveryFailed(String serviceType, int errorCode) {
                Log.e(TAG, "Discovery failed: Error code:" + errorCode);
                nsdManager.stopServiceDiscovery(this);
            }
        };
    }


    public void initializeResolveListener() {
        resolveListener = new NsdManager.ResolveListener() {

            @Override
            public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
                // Called when the resolve fails. Use the error code to debug.
                Log.e(TAG, "Resolve failed: " + errorCode);
            }

            @Override
            public void onServiceResolved(NsdServiceInfo serviceInfo) {
                Log.e(TAG, "Resolve Succeeded. " + serviceInfo);

                if (serviceInfo.getServiceName().equals(serviceName)) {
                    Log.d(TAG, "Same IP.");
                    return;
                }
                mService = serviceInfo;
                int port = mService.getPort();
                //欲しいhost名
                InetAddress host = mService.getHost();
            }
        };
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

docker image「appium/appium」ではandroid webviewのテスト実行がエラーとなるので対処

結論

  1. docker(appium/appium)で立てたappium serverを通してwebviewの項目にswitchするとエラーになる
  2. 対処するにはapium server内にchromedriverをインストールしておく必要がある

エラー:appium-serverにchromedriverをイントールしていない状態

# pytest -s test_main.py 
================================================== test session starts ===================================================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /root/project/appium-script
plugins: forked-1.1.3, xdist-1.31.0
collected 1 item                                                                                                         

    def test_ehlo(self):

        self.driver = webdriver.Remote('http://yourappium-server-address:4723/wd/hub', self.desired_caps)

        webview = self.driver.contexts[-1]
>       self.driver.switch_to.context(webview)

test_main.py:41: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
/usr/local/lib/python3.6/dist-packages/appium/webdriver/switch_to.py:30: in context
    self._driver.execute(MobileCommand.SWITCH_TO_CONTEXT, {'name': context_name})
/usr/local/lib/python3.6/dist-packages/selenium/webdriver/remote/webdriver.py:321: in execute
    self.error_handler.check_response(response)
/usr/local/lib/python3.6/dist-packages/appium/webdriver/errorhandler.py:29: in check_response
    raise wde
/usr/local/lib/python3.6/dist-packages/appium/webdriver/errorhandler.py:24: in check_response
    super(MobileErrorHandler, self).check_response(response)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <appium.webdriver.errorhandler.MobileErrorHandler object at 0x7fcf44ee8908>
response = {'status': 500, 'value': '{"value":{"error":"unknown error","message":"An unknown server-side error occurred while pro...    at asyncHandler (/usr/lib/node_modules/appium/node_modules/appium-base-driver/lib/protocol/protocol.js:388:37)"}}'}

    def check_response(self, response):
        """
        Checks that a JSON response from the WebDriver does not have an error.

        :Args:
         - response - The JSON response from the WebDriver server as a dictionary
           object.

        :Raises: If the response contains an error message.
        """
        status = response.get('status', None)
        if status is None or status == ErrorCode.SUCCESS:
            return
        value = None
        message = response.get("message", "")
        screen = response.get("screen", "")
        stacktrace = None
        if isinstance(status, int):
            value_json = response.get('value', None)
            if value_json and isinstance(value_json, basestring):
                import json
                try:
                    value = json.loads(value_json)
                    if len(value.keys()) == 1:
                        value = value['value']
                    status = value.get('error', None)
                    if status is None:
                        status = value["status"]
                        message = value["value"]
                        if not isinstance(message, basestring):
                            value = message
                            message = message.get('message')
                    else:
                        message = value.get('message', None)
                except ValueError:
                    pass

        exception_class = ErrorInResponseException
        if status in ErrorCode.NO_SUCH_ELEMENT:
            exception_class = NoSuchElementException
        elif status in ErrorCode.NO_SUCH_FRAME:
            exception_class = NoSuchFrameException
        elif status in ErrorCode.NO_SUCH_WINDOW:
            exception_class = NoSuchWindowException
        elif status in ErrorCode.STALE_ELEMENT_REFERENCE:
            exception_class = StaleElementReferenceException
        elif status in ErrorCode.ELEMENT_NOT_VISIBLE:
            exception_class = ElementNotVisibleException
        elif status in ErrorCode.INVALID_ELEMENT_STATE:
            exception_class = InvalidElementStateException
        elif status in ErrorCode.INVALID_SELECTOR \
                or status in ErrorCode.INVALID_XPATH_SELECTOR \
                or status in ErrorCode.INVALID_XPATH_SELECTOR_RETURN_TYPER:
            exception_class = InvalidSelectorException
        elif status in ErrorCode.ELEMENT_IS_NOT_SELECTABLE:
            exception_class = ElementNotSelectableException
        elif status in ErrorCode.ELEMENT_NOT_INTERACTABLE:
            exception_class = ElementNotInteractableException
        elif status in ErrorCode.INVALID_COOKIE_DOMAIN:
            exception_class = InvalidCookieDomainException
        elif status in ErrorCode.UNABLE_TO_SET_COOKIE:
            exception_class = UnableToSetCookieException
        elif status in ErrorCode.TIMEOUT:
            exception_class = TimeoutException
        elif status in ErrorCode.SCRIPT_TIMEOUT:
            exception_class = TimeoutException
        elif status in ErrorCode.UNKNOWN_ERROR:
            exception_class = WebDriverException
        elif status in ErrorCode.UNEXPECTED_ALERT_OPEN:
            exception_class = UnexpectedAlertPresentException
        elif status in ErrorCode.NO_ALERT_OPEN:
            exception_class = NoAlertPresentException
        elif status in ErrorCode.IME_NOT_AVAILABLE:
            exception_class = ImeNotAvailableException
        elif status in ErrorCode.IME_ENGINE_ACTIVATION_FAILED:
            exception_class = ImeActivationFailedException
        elif status in ErrorCode.MOVE_TARGET_OUT_OF_BOUNDS:
            exception_class = MoveTargetOutOfBoundsException
        elif status in ErrorCode.JAVASCRIPT_ERROR:
            exception_class = JavascriptException
        elif status in ErrorCode.SESSION_NOT_CREATED:
            exception_class = SessionNotCreatedException
        elif status in ErrorCode.INVALID_ARGUMENT:
            exception_class = InvalidArgumentException
        elif status in ErrorCode.NO_SUCH_COOKIE:
            exception_class = NoSuchCookieException
        elif status in ErrorCode.UNABLE_TO_CAPTURE_SCREEN:
            exception_class = ScreenshotException
        elif status in ErrorCode.ELEMENT_CLICK_INTERCEPTED:
            exception_class = ElementClickInterceptedException
        elif status in ErrorCode.INSECURE_CERTIFICATE:
            exception_class = InsecureCertificateException
        elif status in ErrorCode.INVALID_COORDINATES:
            exception_class = InvalidCoordinatesException
        elif status in ErrorCode.INVALID_SESSION_ID:
            exception_class = InvalidSessionIdException
        elif status in ErrorCode.UNKNOWN_METHOD:
            exception_class = UnknownMethodException
        else:
            exception_class = WebDriverException
        if value == '' or value is None:
            value = response['value']
        if isinstance(value, basestring):
            if exception_class == ErrorInResponseException:
                raise exception_class(response, value)
            raise exception_class(value)
        if message == "" and 'message' in value:
            message = value['message']

        screen = None
        if 'screen' in value:
            screen = value['screen']

        stacktrace = None
        if 'stackTrace' in value and value['stackTrace']:
            stacktrace = []
            try:
                for frame in value['stackTrace']:
                    line = self._value_or_default(frame, 'lineNumber', '')
                    file = self._value_or_default(frame, 'fileName', '<anonymous>')
                    if line:
                        file = "%s:%s" % (file, line)
                    meth = self._value_or_default(frame, 'methodName', '<anonymous>')
                    if 'className' in frame:
                        meth = "%s.%s" % (frame['className'], meth)
                    msg = "    at %s (%s)"
                    msg = msg % (meth, file)
                    stacktrace.append(msg)
            except TypeError:
                pass
        if exception_class == ErrorInResponseException:
            raise exception_class(response, message)
        elif exception_class == UnexpectedAlertPresentException:
            alert_text = None
            if 'data' in value:
                alert_text = value['data'].get('text')
            elif 'alert' in value:
                alert_text = value['alert'].get('text')
            raise exception_class(message, screen, stacktrace, alert_text)
>       raise exception_class(message, screen, stacktrace)
E       selenium.common.exceptions.WebDriverException: Message: An unknown server-side error occurred while processing the command. Original error: Trying to use a chromedriver binary at the path /usr/bin/chromedriver, but it doesn't exist!

対処:chromedriverを含めた状態でappium-serverを作成(Dockerfile)

FROM    appium/appium

WORKDIR /tmp

RUN     apt-get update && \
        apt-get install -y --no-install-recommends wget unzip

RUN     wget https://chromedriver.storage.googleapis.com/80.0.3987.106/chromedriver_linux64.zip && \
        unzip chromedriver_linux64.zip && \
        mv chromedriver /usr/bin/. && \
        chmod +x /usr/bin/chromedriver

よく考えたら、、、

ホストからchromedriverをマウントして渡せばできたな、、、

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WebViewのvideoで表示されるあの画像(▶)を消す方法

WebViewの動画再生で見たことあるであろう「あの画像」を消す方法のメモ。

ちなみにこちらのことです↓
▶.png

WebChromeClientgetDefaultVideoPoster を指定すればOKだった

WebChromeClientgetDefaultVideoPoster で適当なBitmapを返すことで、実質消すことができるようでした。

override fun getDefaultVideoPoster(): Bitmap? {
    return Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
}

Before/After

▶の画像が消え、チラつかなくなりました。

Before After
1586051504.gif 1586051528.gif

参考

https://developer.android.com/reference/android/webkit/WebChromeClient.html?hl=ru#getDefaultVideoPoster()
https://stackoverflow.com/questions/18271991/html5-video-remove-overlay-play-icon

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GitLab で Unity の Android アプリを自動ビルドするようにしてみた

GitLab で Unity の Android アプリを自動ビルドするようにしてみた

GitLab で Unity の Androidアプリを自動ビルドするようにしてみました。
最初は、Windows10 で試行錯誤していましたが、GitLab Runner の実行ユーザーがシステムアカウントになるためか、エラーが解消されませんでした。
Mac で試したところ問題なく動いたので、Mac を使うほうがよさそうです。

環境

  • macOS Catalina 10.15.4
  • GitLab CE 12.8.6(Windows10上の仮想サーバーで動かしています)
  • GitLab Runner 12.9.0(Mac上で動かしています)
  • Unity 2018.4.20f1

フォルダ構成

ファイル/フォルダ 内容
TestProject_Unity/ Unityプロジェクトフォルダ
TestProject_Unity/.gitignore Git無視リストファイル
TestAutoTest/TestProject_Unity/Assets/Editor/AppBuild.cs ビルド実行メソッドC#ファイル
.gitlab-ci.yml GitLab CI 設定ファイル
build.sh ビルド用シェルスクリプトファイル
README.md 説明ファイル

注意点

Android SDK をインストールしたフォルダに toolsフォルダが入っていないことがあるようです。

Image Pasted at 2020-4-5 09-43.png

toolsフォルダが見当たらないときは、Android Studio から SDKManager を起動して、Android SDK Platform-Tools をインストールしてみてください。

Image Pasted at 2020-4-5 09-43_2.png

ファイルの内容

TestProject_Unity/.gitignore

/[Ll]ibrary/
/[Tt]emp/
/[Oo]bj/
/[Bb]uild/
/[Bb]uilds/
/Assets/AssetStoreTools*

# Autogenerated VS/MD solution and project files
ExportedObj/
*.csproj
*.unityproj
*.sln
*.suo
*.tmp
*.user
*.userprefs
*.pidb
*.booproj
*.svd

# Unity3D generated meta files
*.pidb.meta

# Unity3D Generated File On Crash Reports
sysinfo.txt

# Builds
*.apk
*.unitypackage

# Visual Studio
.vs
Release/
Debug/

.gitlab-ci.yml

stages:
  - build

job_build01:
  stage: build
  script:
    - echo "Start build"
    - chmod a+x ./build.sh
    - ./build.sh Android 1.2.3.4
    - echo "Finish build"
  tags:
    - mac
  artifacts:
    paths:
      - build_*.log
      - TestProject_Unity/Build/
    expire_in: 1 week

TestAutoTest/TestProject_Unity/Assets/Editor/AppBuild.cs

using UnityEditor;
using UnityEngine;
using UnityEditor.Build.Reporting;
using System;
using System.Collections.Generic;

public class AppBuild : MonoBehaviour
{
    static string[] GetSceneList()
    {
        List<string> sceneList = new List<string>();
        EditorBuildSettingsScene[] scenes = EditorBuildSettings.scenes;
        foreach (EditorBuildSettingsScene scene in scenes)
        {
            if (scene.enabled)
            {
                sceneList.Add(scene.path);
            }
        }
        return sceneList.ToArray();
    }

    static string GetArgument(string name)
    {
        string[] arguments = Environment.GetCommandLineArgs();
        for (int i = 0; i < arguments.Length; i++)
        {
            if (arguments[i].Contains(name))
            {
                return arguments[i + 1];
            }
        }
        return null;
    }

    public static void Build()
    {
        string locationPathName = "";
        BuildTarget target = BuildTarget.NoTarget;
        string bundleVersion = GetArgument("/version");;
        string platformName = GetArgument("/platform");
        switch (platformName.ToLower())
        {
            case "android":
                {
                    locationPathName = "Build/" + platformName + "_" + bundleVersion + ".apk";
                }
                break;
            default:
                {
                    locationPathName = "Build/" + platformName + "_" + bundleVersion;
                }
                break;
        }
        target = (BuildTarget)Enum.Parse(typeof(BuildTarget), platformName);

        BuildPlayerOptions buildPlayerOptions = new BuildPlayerOptions();
        buildPlayerOptions.scenes = GetSceneList();
        buildPlayerOptions.locationPathName = locationPathName;
        buildPlayerOptions.target = target;
        buildPlayerOptions.options = BuildOptions.Development;

        PlayerSettings.bundleVersion = bundleVersion;

        BuildReport report = BuildPipeline.BuildPlayer(buildPlayerOptions);
        BuildSummary summary = report.summary;

        if (summary.result == BuildResult.Succeeded)
        {
            Debug.Log("Build succeeded: " + summary.totalSize + " bytes");
        }

        if (summary.result == BuildResult.Failed)
        {
            Debug.Log("Build failed");
        }
    }
}

build.sh

#!/bin/bash

SHELL_PATH=`pwd`
PLATFORM=$1
VERSION=$2

UNITY=/Applications/Unity/Hub/Editor/2018.4.20f1/Unity.app/Contents/MacOS/Unity
LOGFILE=build_${PLATFORM}.log
PROJECT_PATH=TestProject_Unity
EXECUTE_METHOD=AppBuild.Build

${UNITY} -batchmode -quit -logFile ${LOGFILE} -projectPath "${SHELL_PATH}/${PROJECT_PATH}" -executeMethod ${EXECUTE_METHOD} /platform ${PLATFORM} /version ${VERSION}

まとめ

Unity のビルドはけっこう時間がかかりますね。
開発に専念できるように、自動ビルドする環境を用意してはいかがでしょうか?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Flutter】BLoCパターンとは?

BLoCパターンとは?

BLoCパターンは、Business Logic Componentの略
BLoC PatternはFlutterでのアプリケーション開発時に用いる、状態管理手法の1つです。
ビジネスロジックをコンポーネント単位で管理しやすくするためのパターンです。

BLoCのガイドライン

  1. インプットとアウトプットは、単純なStreamとSinkに限定する。(Inputs and outputs are simple Streams/Sinks only.)
  2. 依存性は、必ず注入可能でプラットフォームに依存しないものとする。(Dependencies must be injectable and platform agnostic.)
  3. プラットフォームごとの条件分岐は、許可しない。(No platform branching allowed.)

メモ的に参考資料をまとめておきます。

参考記事

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む