У меня есть приложение, которое я написал, используя документацию 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)
}
}