Тестирование android MediaBrowserService - PullRequest
0 голосов
/ 18 марта 2020

У меня есть приложение, которое я написал, используя документацию Google, и в результате я использую MediaBrowserService и следовал большинству из него инструкций. Сейчас я пытаюсь написать тесты для сервиса, но я никогда раньше не писал тестов для любого моего проекта и не могу понять, с чего и как начать. В моем приложении в настоящее время есть проблема с тем, что repeatMode in Exoplayer не изменяется при запросе из пользовательского интерфейса, и я хочу уловить эту проблему с помощью моего теста.

package com.zenithappworks.dualmusicplayer

import android.annotation.SuppressLint
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.*
import android.graphics.BitmapFactory
import android.media.AudioManager
import android.media.audiofx.Equalizer
import android.os.Bundle
import android.provider.MediaStore
import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.media.MediaBrowserServiceCompat
import androidx.media.session.MediaButtonReceiver
import androidx.preference.PreferenceManager
import com.google.android.exoplayer2.PlaybackParameters
import com.google.android.exoplayer2.Player
import com.zenithappworks.dualmusicplayer.DualAudioPlayerSession.Companion.DUAL_SPEED
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject


/**
 * Service that serves as the backbone for the app. This serves as a liason between the DualAudioPlayerSession and PlayerActivity.
 * Manages the notification, noisy listener and current tracks as well as communications between the player and the UI.
 */

class DualAudioService : MediaBrowserServiceCompat(), DualAudioPlayerSession.PlayerHost {

    private var audioPlayerSession: DualAudioPlayerSession? = null

    private val channelId = "$TAG.Notification"
    private val notificationBuilder: NotificationCompat.Builder by lazy {
        NotificationCompat.Builder(applicationContext, channelId).apply {
                setOnlyAlertOnce(true)
                setSmallIcon(android.R.drawable.stat_sys_headset)
                setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.default_album_art, BitmapFactory.Options().apply { inSampleSize = 4 }))
                setStyle(setupStyle())
                setContentIntent(mediaSession.controller.sessionActivity)
                setDeleteIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(applicationContext, PlaybackStateCompat.ACTION_STOP))
                addAction(
                        R.drawable.exo_icon_previous, "Prev",
                        MediaButtonReceiver.buildMediaButtonPendingIntent(applicationContext, PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS))

                addAction(pauseAction)

                addAction(
                        R.drawable.exo_icon_next, "Next",
                        MediaButtonReceiver.buildMediaButtonPendingIntent(applicationContext, PlaybackStateCompat.ACTION_SKIP_TO_NEXT)
                )
                addAction(
                        android.R.drawable.ic_menu_close_clear_cancel, "Stop",
                        MediaButtonReceiver.buildMediaButtonPendingIntent(applicationContext, PlaybackStateCompat.ACTION_STOP)
                )
        }
    } //TODO Try lazy for all

    private val notificationManagerCompat: NotificationManagerCompat by lazy { NotificationManagerCompat.from(applicationContext) }

    private var isRegisteredForNoisy = false
    private var isForeground = false

    private lateinit var mediaSession: MediaSessionCompat
    private lateinit var broadcastReceiver: BroadcastReceiver
    private lateinit var pauseAction: NotificationCompat.Action
    private lateinit var playAction: NotificationCompat.Action
    private lateinit var playbackStateBuilder: PlaybackStateCompat.Builder
    private lateinit var mediaMetaDataBuilder: MediaMetadataCompat.Builder

    private lateinit var sharedPreferences: SharedPreferences
    //===========================================================Lifecycle Methods==================================================================

    //TODO Playback restarted on playeractivity resuming twice.


    override fun onCreate() {
        super.onCreate()
        Log.d(TAG, "onCreate: Starts")

        playAction = NotificationCompat.Action(R.drawable.exo_icon_play, "Play", MediaButtonReceiver.buildMediaButtonPendingIntent(applicationContext, PlaybackStateCompat.ACTION_PLAY))
        pauseAction = NotificationCompat.Action(R.drawable.exo_icon_pause, "Pause", MediaButtonReceiver.buildMediaButtonPendingIntent(applicationContext, PlaybackStateCompat.ACTION_PAUSE))


        playbackStateBuilder = PlaybackStateCompat.Builder()
        mediaMetaDataBuilder = MediaMetadataCompat.Builder()

        sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext)

        broadcastReceiver = setupNoisyReceiver()

        Log.d(TAG, "onCreate: Session setup")

        setupMediaSession()

        Log.d(TAG, "onCreate: $mediaSession")

        sessionToken = mediaSession.sessionToken
    }

    override fun onDestroy() {
        storePlayState()
        if (mediaSession.isActive) {
            stopAsForeground(true)
            mediaSession.isActive = false
            mediaSession.release()
        }
        super.onDestroy()
    }

    private fun storePlayState() {
        sharedPreferences.edit()
                .putString(PlayerActivity.LAST_PLAYING_QUEUE, playlist.toString())
                .putInt(PlayerActivity.LAST_PLAYING_POSITION, currentTrack)
                .putInt(PlayerActivity.REPEAT_MODE, mediaSession.controller.repeatMode)
                .putBoolean(PlayerActivity.LAST_QUEUE_REMEMBERED, true)
                .apply()
    }

    private fun setupMediaSession() {
        Log.d(TAG, "setupMediaSession: Setting up")
        mediaSession = MediaSessionCompat(applicationContext, TAG)
        mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS)
        audioPlayerSession = DualAudioPlayerSession(applicationContext, this)
        mediaSession.setCallback(audioPlayerSession)
        mediaSession.setMediaButtonReceiver(MediaButtonReceiver.buildMediaButtonPendingIntent(applicationContext, PlaybackStateCompat.ACTION_PLAY))
        val intent = Intent(applicationContext, PlayerActivity::class.java)
        mediaSession.setSessionActivity(
                PendingIntent.getActivity(
                        applicationContext, 0,
                        intent,
                        PendingIntent.FLAG_UPDATE_CURRENT
                )
        )

        mediaSession.setRepeatMode(sharedPreferences.getInt(PlayerActivity.REPEAT_MODE, PlaybackStateCompat.REPEAT_MODE_ALL))
        updatePlaybackState(PlaybackStateCompat.STATE_STOPPED, PlaybackStateCompat.ACTION_PREPARE)

        mediaSession.isActive = true
        Log.d(TAG, "setupMediaSession: Ends")
    }

    private fun startAsForeground() {
        startService(Intent(applicationContext, DualAudioService::class.java))
        startForeground(NOTIFY_ID, setupNotification())
        isForeground = true
    }

    private fun stopAsForeground(removeNotification: Boolean) {
        stopForeground(removeNotification)
        isForeground = false
    }

    //=============================================================BROADCAST RECEIVER============================================================

    private fun setupNoisyReceiver(): BroadcastReceiver {
        return object : BroadcastReceiver() {
            override fun onReceive(context: Context, intent: Intent) {
                if (AudioManager.ACTION_AUDIO_BECOMING_NOISY == intent.action) {
                    mediaSession.controller.transportControls.pause()
                }
            }
        }
    }

    override fun registerReceiver(receiver: BroadcastReceiver?, filter: IntentFilter): Intent? {
        isRegisteredForNoisy = true
        return super.registerReceiver(receiver, filter)
    }

    override fun unregisterReceiver(receiver: BroadcastReceiver) {
        isRegisteredForNoisy = false
        super.unregisterReceiver(receiver)
    }

    //=======================================================NOTIFICATIONS================================================================================
    @SuppressLint("RestrictedApi")
    private fun updateNotification(state: Int) {

        val nowPlayingOne = playlist!!.getJSONObject(currentTrack)
        var nowPlayingTwo = JSONObject()
        if (isDualMode) {
            nowPlayingTwo = playlist!!.getJSONObject(currentTrack2)
        }

        try {
            notificationBuilder
                    .setContentTitle(
                            if (isDualMode)
                                "Dual Mode"
                            else
                                nowPlayingOne!!.getString(MediaStore.Audio.Media.TITLE)
                    )
                    .setContentText(
                            if (isDualMode)
                                nowPlayingOne!!.getString(MediaStore.Audio.Media.TITLE)
                            else
                                nowPlayingOne!!.getString(MediaStore.Audio.Media.ARTIST)
                    )
                    .setSubText(
                            if (isDualMode)
                                nowPlayingTwo.getString(MediaStore.Audio.Media.TITLE)
                            else
                                nowPlayingOne.getString(MediaStore.Audio.Media.ALBUM)
                    )
            val indexOf: Int
            when (state) {
                PlaybackStateCompat.STATE_PLAYING -> if (notificationBuilder.mActions.contains(playAction)) {
                    indexOf = notificationBuilder.mActions.indexOf(playAction)
                    notificationBuilder.mActions.removeAt(indexOf)
                    notificationBuilder.mActions.add(indexOf, pauseAction)
                }
                PlaybackStateCompat.STATE_STOPPED, PlaybackStateCompat.STATE_PAUSED -> if (notificationBuilder.mActions.contains(pauseAction)) {
                    indexOf = notificationBuilder.mActions.indexOf(pauseAction)
                    notificationBuilder.mActions.removeAt(indexOf)
                    notificationBuilder.mActions.add(indexOf, playAction)
                }
            }
        } catch (j: JSONException) {
            j.printStackTrace()
        }

        notificationManagerCompat.notify(NOTIFY_ID, notificationBuilder.build())
    }

    private fun setupNotification(): Notification {
        val channelId = "$TAG.Notification"
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
            val notificationChannel = NotificationChannel(channelId, TAG, NotificationManager.IMPORTANCE_DEFAULT)
            val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            notificationManager.createNotificationChannel(notificationChannel)
        }

        try {

            val nowPlayinOne = playlist!!.getJSONObject(currentTrack)
            var nowPlayingTwo = JSONObject()
            if (isDualMode) {
                nowPlayingTwo = playlist!!.getJSONObject(currentTrack2)
            }

            notificationBuilder
                    .setContentTitle(
                            if (isDualMode)
                                "Dual Mode"
                            else
                                nowPlayinOne!!.getString(MediaStore.Audio.Media.TITLE)
                    )
                    .setContentText(
                            if (isDualMode)
                                nowPlayinOne!!.getString(MediaStore.Audio.Media.TITLE)
                            else
                                nowPlayinOne!!.getString(MediaStore.Audio.Media.ARTIST)
                    )
                    .setSubText(
                            if (isDualMode)
                                nowPlayingTwo.getString(MediaStore.Audio.Media.TITLE)
                            else
                                nowPlayinOne.getString(MediaStore.Audio.Media.ALBUM)
                    )

        } catch (e: JSONException) {
            e.printStackTrace()
        }

        return notificationBuilder.build()
    }

    private fun setupStyle(): androidx.media.app.NotificationCompat.MediaStyle {
        val mediaStyle = androidx.media.app.NotificationCompat.MediaStyle()
        mediaStyle.setMediaSession(mediaSession.sessionToken)
        return mediaStyle
    }

    //=======================================================PlAYER STATE MANAGEMENT================================================================================

    internal fun updatePlaybackState(state: Int, action: Long) {
        val dualBundle = Bundle()
        playbackStateBuilder
                .setState(state, audioPlayerSession!!.currentPositionOne, audioPlayerSession!!.getPlaybackParametersFor(1).speed)
                .setActions(action or PlaybackStateCompat.ACTION_SKIP_TO_NEXT or PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or PlaybackStateCompat.ACTION_STOP)
        if (isDualMode) {
            dualBundle.putFloat(DUAL_SPEED, audioPlayerSession!!.getPlaybackParametersFor(2).speed)
            dualBundle.putFloat(PlaylistDialog.PLAYERTWO_PITCH, audioPlayerSession!!.getPlaybackParametersFor(2).pitch)
        }
        dualBundle.putFloat(PlaylistDialog.PLAYERONE_PITCH, audioPlayerSession!!.getPlaybackParametersFor(1).pitch)
        playbackStateBuilder.setExtras(dualBundle)
        mediaSession.setPlaybackState(playbackStateBuilder.build())
    }

    override fun updateMetaData() {
        try {
            val nowPlayingOne = playlist!!.getJSONObject(currentTrack)
            var nowPlayingTwo: JSONObject? = null
            if (isDualMode) {
                nowPlayingTwo = playlist!!.getJSONObject(currentTrack2)
            }
            mediaMetaDataBuilder
                    .putString(
                            MediaMetadataCompat.METADATA_KEY_TITLE,
                            if (isDualMode)
                                nowPlayingOne.getString(MediaStore.Audio.Media.TITLE) + " + "
                                        + nowPlayingTwo!!.getString(MediaStore.Audio.Media.TITLE)
                            else
                                nowPlayingOne.getString(MediaStore.Audio.Media.TITLE)
                    )

                    .putString(
                            MediaMetadataCompat.METADATA_KEY_ARTIST,
                            if (isDualMode) nowPlayingOne.getString(MediaStore.Audio.Media.ARTIST) + " + " + nowPlayingTwo!!.getString(MediaStore.Audio.Media.ARTIST) else nowPlayingOne.getString(MediaStore.Audio.Media.ARTIST)
                    )
                    .putString(
                            MediaMetadataCompat.METADATA_KEY_ALBUM,
                            if (isDualMode)
                                nowPlayingOne.getString(MediaStore.Audio.Media.ALBUM) + " + " + nowPlayingTwo!!.getString(MediaStore.Audio.Media.ALBUM)
                            else
                                nowPlayingOne.getString(MediaStore.Audio.Media.ALBUM)
                    )
                    .putString(
                            MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI,
                            if (isDualMode)
                                nowPlayingOne.getString(MediaStore.Audio.Media.ALBUM_ID) + " + " + nowPlayingTwo!!.getString(MediaStore.Audio.Media.ALBUM_ID)
                            else
                                nowPlayingOne.getString(MediaStore.Audio.Media.ALBUM_ID)
                    )
                    .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID,
                            if (isDualMode)
                                nowPlayingOne.getString(MediaStore.Audio.Media._ID) + "," + nowPlayingTwo!!.getString(MediaStore.Audio.Media._ID)
                            else
                                nowPlayingOne.getString(MediaStore.Audio.Media._ID)
                    )

                    .putLong(
                            MediaMetadataCompat.METADATA_KEY_DURATION,
                            nowPlayingOne.getLong(MediaStore.Audio.Media.DURATION)
                    )
        } catch (e: JSONException) {
            e.printStackTrace()
        }

        mediaSession.setMetadata(mediaMetaDataBuilder.build())
    }

    override fun getCurrentTrack(trackNo: Int): JSONObject {
        return when (trackNo) {
            1 -> playlist!!.getJSONObject(currentTrack)
            2 -> playlist!!.getJSONObject(currentTrack2)
            else -> playlist!!.getJSONObject(currentTrack)
        }
    }

    override fun nextTrack() {
        currentTrack++
        if (currentTrack == playlist?.length()) {
            currentTrack = 0
        }
    }

    override fun prevTrack() {
        currentTrack--
        if (currentTrack < 0 && playlist != null) {
            currentTrack = playlist!!.length() - 1
        }
    }

    override fun onGetRoot(s: String, i: Int, bundle: Bundle?): BrowserRoot? {
        return BrowserRoot(TAG, null)
    }

    override fun onLoadChildren(parentId: String, result: Result<List<MediaBrowserCompat.MediaItem>>) {

    }

    companion object {
        private const val TAG = "DualAudioService"
        private const val NOTIFY_ID = 1
        var isDualMode: Boolean = false

        var playlist: JSONArray? = null
        var currentTrack = -1
        var currentTrack2 = -1
            set(value) {
                field = value
                isDualMode = true
            }

        lateinit var equalizer: Equalizer
            private set


    }

    override fun onPrepare() {
        startAsForeground()
    }

    override fun onRepeatModeChange(playbackState: Int) {
        mediaSession.setRepeatMode(playbackState)
    }

    override fun onPlayParameterChange(playbackParameters: PlaybackParameters) {
        val bundle = Bundle()
        bundle.putFloat(PlaylistDialog.PLAYERONE_PITCH, playbackParameters.pitch)
        bundle.putFloat(PlaylistDialog.PLAYERONE_SPEED, playbackParameters.speed)
        playbackStateBuilder.setExtras(bundle)
        mediaSession.setPlaybackState(playbackStateBuilder.build())
    }

    override fun onShuffleChange(playbackState: Int) {
        mediaSession.setShuffleMode(playbackState)
    }

    override fun onPlayerStateChange(playbackState: Int) {
        when (playbackState) {
            PlaybackStateCompat.STATE_PLAYING -> updatePlaybackState(playbackState, PlaybackStateCompat.ACTION_PAUSE)
            PlaybackStateCompat.STATE_PAUSED -> updatePlaybackState(playbackState, PlaybackStateCompat.ACTION_PLAY)
            PlaybackStateCompat.STATE_STOPPED -> updatePlaybackState(playbackState, PlaybackStateCompat.ACTION_PLAY)
            PlaybackStateCompat.STATE_BUFFERING -> updatePlaybackState(playbackState, PlaybackStateCompat.ACTION_PLAY)
        }
    }

    override fun onTrackChange(reason: Int) { //TODO("Need better implementation")
        when (reason) {
            Player.DISCONTINUITY_REASON_PERIOD_TRANSITION -> {
                Log.d(TAG, "onPositionDiscontinuity: transition called")
                if (mediaSession.controller.repeatMode != PlaybackStateCompat.REPEAT_MODE_ONE) {
                    nextTrack()
                    audioPlayerSession!!.updateNowPlaying()
                }
                Log.d(TAG, "onPositionDiscontinuity: seek called")
                updateMetaData()
                updatePlaybackState(PlaybackStateCompat.STATE_PLAYING, PlaybackStateCompat.ACTION_PAUSE)
                updateNotification(PlaybackStateCompat.STATE_PLAYING)
            }
            Player.DISCONTINUITY_REASON_SEEK -> {
                Log.d(TAG, "onPositionDiscontinuity: seek called")
                updateMetaData()
                updatePlaybackState(PlaybackStateCompat.STATE_PLAYING, PlaybackStateCompat.ACTION_PAUSE)
                updateNotification(PlaybackStateCompat.STATE_PLAYING)
            }
            Player.DISCONTINUITY_REASON_AD_INSERTION, Player.DISCONTINUITY_REASON_INTERNAL, Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT -> {
            }
        }
    }

    override fun onSessionIDReceived(audioSessionID: Int) {
        equalizer = Equalizer(0, audioSessionID)
    }

}
...