- 投稿日:2020-04-05T23:48:49+09:00
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を呼び出せば音楽が再生される。
まとめ
質問を随時受付中。
この記事は自分のために高速で作ったものなので不備ありありだが、質問されれれば全力で答えていく。
- 投稿日:2020-04-05T22:19:17+09:00
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の世界に突入しそうでしたが...
- 投稿日:2020-04-05T20:17:26+09:00
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.エラーの原因
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確認ができたら、もう一度flutter runを実行すると無事にアプリが起動します。
flutter run最後に
解説は以上となります。
最後までご覧くださりありがとうございました。
- 投稿日:2020-04-05T17:58:10+09:00
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を使用してビットコードをターゲットにすることはサポートされておらず、機能しない可能性があります。 ]
2.最新Ver.のXcodeを使用するようにXcodeコマンドラインツールを構成
ターミナルにて以下を実行します。
パスワードを求められるので入力します。sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer sudo xcodebuild -runFirstLaunch3.Xcodeライセンスの確認
以下を実行し内容を確認します。
spaceキーで下にスクロールすることができます。
qキーでページを抜けることができます。sudo xcodebuild -licenseページを抜けた後、以下を入力することでライセンスに同意します。
agreeiOSシミュレーターのセットアップ
1.iOSシミュレーターを実行
ターミナルにて以下を実行します。
open -a SimulatorFlutterアプリを作成し実行する
シンプルなFlutterアプリを作成し、実行していきます。
1.ディレクトリ作成
今回のサンプルアプリ用にディレクトリを作成します。
各々好きな名前で構いません。mkdir 好きな名前2.作成したディレクトリへ移動
cd 先ほど作成したディレクトリ名3.Flutterアプリ作成
以下コマンドを入力すると、
先ほど作成したディレクトリの中に
my_appというサンプルのFlutterアプリが作成されます。flutter create my_app4.作成したmy_appへ移動
cd my_app5.iOSシミュレーターでmy_appを実行
iOSシミュレーターを起動した状態で以下を実行します。
flutter runするとiOSシミュレーター内でmy_appが実行されます。
右下の+ボタンを押すと数字がカウントアップされます。
ターミナルでqキーを入力するとアプリを終了できます。
これでシンプルなFlutterアプリの作成・実行ができました!最後に
以上で「iOSシミュレーター実行編」は終了となります。お疲れさまでした。
実機にてアプリの確認をしたい場合はFlutter公式サイトの
「Deploy to iOS devices」欄を参考にしてみてください。
私はiPhoneユーザーではないので。(iPhone欲しい。。。)最後までご覧くださりありがとうございました。
参考サイト[Flutter公式サイト]
- 投稿日:2020-04-05T17:58:10+09:00
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を使用してビットコードをターゲットにすることはサポートされておらず、機能しない可能性があります。 ]
2.最新Ver.のXcodeを使用するようにXcodeコマンドラインツールを構成
ターミナルにて以下を実行します。
パスワードを求められるので入力します。sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer sudo xcodebuild -runFirstLaunch3.Xcodeライセンスの確認
以下を実行し内容を確認します。
spaceキーで下にスクロールすることができます。
qキーでページを抜けることができます。sudo xcodebuild -licenseページを抜けた後、以下を入力することでライセンスに同意します。
agreeiOSシミュレーターのセットアップ
1.iOSシミュレーターを実行
ターミナルにて以下を実行します。
open -a SimulatorFlutterアプリを作成し実行する
シンプルなFlutterアプリを作成し、実行していきます。
1.ディレクトリ作成
今回のサンプルアプリ用にディレクトリを作成します。
各々好きな名前で構いません。mkdir 好きな名前2.作成したディレクトリへ移動
cd 先ほど作成したディレクトリ名3.Flutterアプリ作成
以下コマンドを入力すると、
先ほど作成したディレクトリの中に
my_appというサンプルのFlutterアプリが作成されます。flutter create my_app4.作成したmy_appへ移動
cd my_app5.iOSシミュレーターでmy_appを実行
iOSシミュレーターを起動した状態で以下を実行します。
flutter runするとiOSシミュレーター内でmy_appが実行されます。
右下の+ボタンを押すと数字がカウントアップされます。
ターミナルでqキーを入力するとアプリを終了できます。
これでシンプルなFlutterアプリの作成・実行ができました!最後に
以上で「Xcode・iOSシミュレーターセットアップ編」は終了となります。お疲れさまでした。
実機にてアプリの確認をしたい場合はFlutter公式サイトの
「Deploy to iOS devices」欄を参考にしてみてください。
私はiPhoneユーザーではないので。(iPhone欲しい。。。)最後までご覧くださりありがとうございました。
参考サイト[Flutter公式サイト]
- 投稿日:2020-04-05T17:47:54+09:00
mDNSでAndroidからRaspberryPiを探す
Raspberry Pi
avahi-daemonの設定
avahi-daemonのインストール(入っていなければ)
sudo apt-get install avahi-daemon設定変更
sudo vi /etc/avahi/avahi-daemon.confnoになっているとAndroid側で探索が出来なかった
avahi-daemon.conf[publish] publish-workstation=yesavahi-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.javapublic 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(); } }; } }
- 投稿日:2020-04-05T15:44:28+09:00
docker image「appium/appium」ではandroid webviewのテスト実行がエラーとなるので対処
結論
- docker(appium/appium)で立てたappium serverを通してwebviewの項目にswitchするとエラーになる
- 対処するには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をマウントして渡せばできたな、、、
- 投稿日:2020-04-05T13:14:41+09:00
WebViewのvideoで表示されるあの画像(▶)を消す方法
WebViewの動画再生で見たことあるであろう「あの画像」を消す方法のメモ。
WebChromeClient
のgetDefaultVideoPoster
を指定すればOKだった
WebChromeClient
のgetDefaultVideoPoster
で適当なBitmapを返すことで、実質消すことができるようでした。override fun getDefaultVideoPoster(): Bitmap? { return Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) }Before/After
▶の画像が消え、チラつかなくなりました。
Before After 参考
https://developer.android.com/reference/android/webkit/WebChromeClient.html?hl=ru#getDefaultVideoPoster()
https://stackoverflow.com/questions/18271991/html5-video-remove-overlay-play-icon
- 投稿日:2020-04-05T09:54:41+09:00
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フォルダが入っていないことがあるようです。
toolsフォルダが見当たらないときは、Android Studio から SDKManager を起動して、Android SDK Platform-Tools をインストールしてみてください。
ファイルの内容
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 weekTestAutoTest/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 のビルドはけっこう時間がかかりますね。
開発に専念できるように、自動ビルドする環境を用意してはいかがでしょうか?
- 投稿日:2020-04-05T02:33:32+09:00
【Flutter】BLoCパターンとは?
BLoCパターンとは?
BLoCパターンは、Business Logic Componentの略
BLoC PatternはFlutterでのアプリケーション開発時に用いる、状態管理手法の1つです。
ビジネスロジックをコンポーネント単位で管理しやすくするためのパターンです。BLoCのガイドライン
- インプットとアウトプットは、単純なStreamとSinkに限定する。(Inputs and outputs are simple Streams/Sinks only.)
- 依存性は、必ず注入可能でプラットフォームに依存しないものとする。(Dependencies must be injectable and platform agnostic.)
- プラットフォームごとの条件分岐は、許可しない。(No platform branching allowed.)
メモ的に参考資料をまとめておきます。
参考記事