Files
SoundWaveRadio/app/src/main/java/com/yaros/RadioUrl/PlayerService.kt
T
2026-02-05 13:47:39 +03:00

887 lines
36 KiB
Kotlin

package com.yaros.RadioUrl
import android.app.ActivityManager
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.Bitmap
import android.media.AudioManager
import android.net.Uri
import android.os.BatteryManager
import android.os.Binder
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.widget.Toast
import androidx.core.app.NotificationCompat
import androidx.media3.*
import androidx.media3.common.AudioAttributes
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.Metadata
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DataSource
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.SeekParameters
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService
import androidx.media3.session.SessionCommand
import androidx.media3.session.SessionResult
import androidx.media3.ui.PlayerNotificationManager
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import com.yaros.RadioUrl.helpers.AudioHelper
import com.yaros.RadioUrl.helpers.NetworkHelper
import com.yaros.RadioUrl.helpers.NetworkMonitor
import com.yaros.RadioUrl.helpers.PreferencesHelper
import com.yaros.RadioUrl.helpers.VolumeSettingsHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import timber.log.Timber
@UnstableApi
class PlayerService : MediaSessionService(), Player.Listener {
private var exoPlayer: ExoPlayer? = null
private lateinit var mediaSession: MediaSession
private lateinit var notificationManager: NotificationManager
private lateinit var audioManager: AudioManager
private lateinit var networkMonitor: NetworkMonitor
private var isForegroundService = false
private lateinit var playerNotificationManager: PlayerNotificationManager
private var hasAudioFocus = false
private var playbackDelayed = false
private var resumeOnFocusGain = false
private val audioFocusRequest = AudioManager.OnAudioFocusChangeListener { focusChange ->
Timber.tag("PlayerService").i("Audio focus changed: $focusChange")
when (focusChange) {
AudioManager.AUDIOFOCUS_GAIN -> {
Timber.tag("PlayerService").i("Audio focus gained")
hasAudioFocus = true
exoPlayer?.let { player ->
// Восстанавливаем громкость если была уменьшена (ducking)
if (player.volume < 1.0f) {
player.volume = 1.0f
Timber.tag("PlayerService").i("Volume restored to 100%")
}
// Если воспроизведение было отложено - запускаем
if (playbackDelayed) {
Timber.tag("PlayerService").i("Starting delayed playback")
player.play()
playbackDelayed = false
}
// Если нужно возобновить после временной паузы
else if (resumeOnFocusGain) {
Timber.tag("PlayerService").i("Resuming playback after transient loss")
player.play()
resumeOnFocusGain = false
}
}
}
AudioManager.AUDIOFOCUS_LOSS -> {
Timber.tag("PlayerService").i("Audio focus lost permanently")
hasAudioFocus = false
resumeOnFocusGain = false
playbackDelayed = false
exoPlayer?.pause()
// Останавливаем сервис при постоянной потере фокуса
stopServiceGracefully()
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
Timber.tag("PlayerService").i("Audio focus lost temporarily")
hasAudioFocus = false
resumeOnFocusGain = exoPlayer?.isPlaying == true
exoPlayer?.pause()
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
Timber.tag("PlayerService").i("Audio focus lost, can duck")
// Уменьшаем громкость вместо паузы
exoPlayer?.volume = 0.2f
}
}
}
inner class LocalBinder : Binder() {
val service: PlayerService
get() = this@PlayerService
}
override fun onCreate() {
super.onCreate()
initComponents()
// Немедленно запускаем foreground service с временным уведомлением
// чтобы избежать RemoteServiceException
startForeground(NOTIFICATION_ID, createInitialNotification())
isForegroundService = true
setupPlayer() // Синхронная инициализация
setupMediaSession() // Должна быть после setupPlayer()
setupNotificationManager() // Должна быть после setupMediaSession()
setupNetworkMonitor()
}
private fun initComponents() {
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
networkMonitor = NetworkMonitor(this)
createNotificationChannel()
}
private fun createDataSourceFactory(): DataSource.Factory {
return try {
OkHttpDataSource.Factory(NetworkHelper.client)
.setUserAgent("SoundWaveRadio/1.0")
} catch (e: Exception) {
Timber.tag("PlayerService").e("Error creating data source factory: ${e.message}")
// Fallback to default
OkHttpDataSource.Factory(NetworkHelper.client)
.setUserAgent("SoundWaveRadio/1.0")
}
}
private fun setupPlayer() {
try {
val dataSourceFactory = DefaultDataSource.Factory(
this,
createDataSourceFactory()
)
exoPlayer = ExoPlayer.Builder(this)
.setHandleAudioBecomingNoisy(true) // Автоматически останавливает при отключении наушников
.setWakeMode(C.WAKE_MODE_NETWORK)
.setLoadControl(
DefaultLoadControl.Builder()
.setBufferDurationsMs(
MIN_BUFFER_MS,
MAX_BUFFER_MS,
PLAYBACK_BUFFER_MS,
REBUFFER_MS
)
.setPrioritizeTimeOverSizeThresholds(true)
.build()
)
.setMediaSourceFactory(
DefaultMediaSourceFactory(dataSourceFactory)
)
.build()
.apply {
addListener(this@PlayerService)
setAudioAttributes(
AudioAttributes.Builder()
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
.setUsage(C.USAGE_MEDIA)
.build(),
false // НЕ обрабатываем Audio Focus автоматически - делаем это вручную
)
playWhenReady = true
setSeekParameters(SeekParameters.CLOSEST_SYNC)
trackSelectionParameters = trackSelectionParameters
.buildUpon()
.setForceHighestSupportedBitrate(true)
.build()
}
Timber.tag("PlayerService").i("ExoPlayer initialized successfully")
} catch (e: Exception) {
Timber.tag("PlayerService").e("Error setting up player: ${e.message}")
throw e
}
}
private fun setupMediaSession() {
mediaSession = MediaSession.Builder(this, exoPlayer!!)
.setSessionActivity(getSessionActivity())
.setCallback(MediaSessionCallback())
.build()
}
private fun setupNotificationManager() {
playerNotificationManager = PlayerNotificationManager.Builder(
this,
NOTIFICATION_ID,
NOTIFICATION_CHANNEL_ID
)
.setMediaDescriptionAdapter(NotificationMediaAdapter())
.setChannelNameResourceId(R.string.notification_info)
.setChannelDescriptionResourceId(R.string.default_notification_channel_name)
.setNotificationListener(object : PlayerNotificationManager.NotificationListener {
override fun onNotificationPosted(
notificationId: Int,
notification: Notification,
ongoing: Boolean
) {
// Обновляем уведомление, если сервис уже в foreground режиме
if (ongoing && isForegroundService) {
startForeground(notificationId, notification)
} else if (!ongoing) {
stopForeground(false)
isForegroundService = false
}
}
override fun onNotificationCancelled(notificationId: Int, dismissedByUser: Boolean) {
stopSelf()
}
})
.build()
.apply {
setPlayer(exoPlayer)
setColorized(true)
setPriority(NotificationCompat.PRIORITY_LOW)
}
}
private val notificationListener = object : PlayerNotificationManager.NotificationListener {
override fun onNotificationPosted(
notificationId: Int,
notification: Notification,
ongoing: Boolean
) {
if (ongoing && !isForegroundService) {
startForeground(notificationId, notification)
isForegroundService = true
}
}
override fun onNotificationCancelled(notificationId: Int, dismissedByUser: Boolean) {
stopSelf()
}
}
private fun setupNetworkMonitor() {
networkMonitor.addListener(object : NetworkMonitor.Listener {
override fun onNetworkAvailable() {
// Post to the main thread
Handler(Looper.getMainLooper()).post {
exoPlayer?.prepare()
exoPlayer?.play()
}
}
override fun onNetworkLost() {
// Post to the main thread
Handler(Looper.getMainLooper()).post {
exoPlayer?.pause()
}
}
})
networkMonitor.start()
}
private fun getSessionActivity(): PendingIntent {
val intent = Intent(this, MainActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
val pendingIntentFlags = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
}
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
}
else -> {
PendingIntent.FLAG_UPDATE_CURRENT
}
}
return PendingIntent.getActivity(
this,
0,
intent,
pendingIntentFlags
)
}
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
return if (::mediaSession.isInitialized) mediaSession else null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
// Убеждаемся, что сервис в foreground режиме при каждом вызове startForegroundService()
if (!isForegroundService) {
startForeground(NOTIFICATION_ID, createInitialNotification())
isForegroundService = true
}
intent?.let {
when (it.action) {
Keys.ACTION_PLAY_STREAM -> {
val streamUri = it.getStringExtra(Keys.EXTRA_STREAM_URI)
val stationName = it.getStringExtra(Keys.EXTRA_STATION_NAME)
val stationImage = it.getStringExtra(Keys.EXTRA_STATION_IMAGE)
val stationUuid = it.getStringExtra(Keys.EXTRA_STATION_UUID)
Timber.tag("PlayerService").i("Received ACTION_PLAY_STREAM: name=$stationName, uri=$streamUri, uuid=$stationUuid")
if (!streamUri.isNullOrBlank()) {
val artUri = if (!stationImage.isNullOrBlank()) {
android.net.Uri.parse(stationImage)
} else {
null
}
playRadio(
url = streamUri,
title = stationName ?: "Radio Station",
artist = getString(R.string.app_name),
artUri = artUri,
stationUuid = stationUuid
)
} else {
Timber.tag("PlayerService").e("Stream URI is null or blank")
}
}
}
}
return START_STICKY
}
override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
Timber.tag("PlayerService").i("Task removed, isPlaying: ${exoPlayer?.isPlaying}")
// Останавливаем сервис только если не воспроизводится
if (exoPlayer?.isPlaying != true) {
Timber.tag("PlayerService").i("Stopping service - not playing")
stopServiceGracefully()
} else {
Timber.tag("PlayerService").i("Keeping service alive - still playing")
}
}
override fun onDestroy() {
releaseResources()
super.onDestroy()
}
private fun releaseResources() {
try {
retryHandler.removeCallbacksAndMessages(null)
retryAttempts = 0
networkMonitor.stop()
// Освобождаем Audio Focus
if (hasAudioFocus) {
audioManager.abandonAudioFocus(audioFocusRequest)
hasAudioFocus = false
Timber.tag("PlayerService").i("Audio focus abandoned")
}
if (::mediaSession.isInitialized) {
mediaSession.release()
}
exoPlayer?.release()
exoPlayer = null
if (::playerNotificationManager.isInitialized) {
playerNotificationManager.setPlayer(null)
}
} catch (e: Exception) {
Timber.tag("PlayerService").e("Error releasing resources: ${e.message}")
}
}
private inner class MediaSessionCallback : MediaSession.Callback {
override fun onConnect(
session: MediaSession,
controller: MediaSession.ControllerInfo
): MediaSession.ConnectionResult {
val baseResult = super.onConnect(session, controller)
val customCommands = listOf(
SessionCommand(Keys.CMD_START_SLEEP_TIMER, Bundle.EMPTY),
SessionCommand(Keys.CMD_CANCEL_SLEEP_TIMER, Bundle.EMPTY),
SessionCommand(Keys.CMD_REQUEST_SLEEP_TIMER_REMAINING, Bundle.EMPTY),
SessionCommand(Keys.CMD_REQUEST_METADATA_HISTORY, Bundle.EMPTY)
)
val newSessionCommands = baseResult.availableSessionCommands
.buildUpon()
.apply { customCommands.forEach { add(it) } }
.build()
return MediaSession.ConnectionResult.accept(
newSessionCommands,
baseResult.availablePlayerCommands
)
}
override fun onCustomCommand(
session: MediaSession,
controller: MediaSession.ControllerInfo,
customCommand: SessionCommand,
args: Bundle
): ListenableFuture<SessionResult> {
when (customCommand.customAction) {
"play" -> {
if (isAppInForeground()) {
// Запрашиваем Audio Focus перед воспроизведением
val result = audioManager.requestAudioFocus(
audioFocusRequest,
AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN
)
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
hasAudioFocus = true
exoPlayer?.play()
Timber.tag("PlayerService").i("Play command: Audio focus granted, playing")
} else {
Timber.tag("PlayerService").w("Play command: Audio focus denied")
}
} else {
Timber.tag("PlayerService").w("Невозможно воспроизвести из фонового режима")
}
}
"pause" -> handlePauseCommand()
"stop" -> stopServiceGracefully()
Keys.CMD_REQUEST_METADATA_HISTORY -> {
val metadataHistory = PreferencesHelper.loadMetadataHistory()
val resultBundle = Bundle().apply {
putStringArrayList(Keys.EXTRA_METADATA_HISTORY, ArrayList(metadataHistory))
}
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS, resultBundle))
}
}
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
}
private fun handlePauseCommand() {
exoPlayer?.pause()
// Освобождаем Audio Focus при паузе
if (hasAudioFocus) {
audioManager.abandonAudioFocus(audioFocusRequest)
hasAudioFocus = false
Timber.tag("PlayerService").i("Audio focus abandoned on pause")
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
stopForeground(STOP_FOREGROUND_DETACH)
} else {
@Suppress("DEPRECATION")
stopForeground(false)
}
isForegroundService = false
}
}
override fun onIsPlayingChanged(isPlaying: Boolean) {
super.onIsPlayingChanged(isPlaying)
Timber.tag("PlayerService").i("onIsPlayingChanged: $isPlaying")
if (!isPlaying) {
// Когда воспроизведение останавливается, освобождаем Audio Focus
if (hasAudioFocus && exoPlayer?.playWhenReady == false) {
audioManager.abandonAudioFocus(audioFocusRequest)
hasAudioFocus = false
Timber.tag("PlayerService").i("Audio focus abandoned - playback stopped")
}
}
}
override fun onPlaybackStateChanged(playbackState: Int) {
try {
when (playbackState) {
Player.STATE_READY -> {
Timber.tag("PlayerService").i("Playback state: READY")
retryAttempts = 0 // Reset retry counter on successful playback
handlePlaybackReady()
}
Player.STATE_ENDED -> {
Timber.tag("PlayerService").i("Playback state: ENDED")
handlePlaybackEnd()
}
Player.STATE_IDLE -> {
Timber.tag("PlayerService").i("Playback state: IDLE")
// Останавливаем сервис если плеер не должен воспроизводить
if (exoPlayer?.playWhenReady == false) {
// Освобождаем Audio Focus
if (hasAudioFocus) {
audioManager.abandonAudioFocus(audioFocusRequest)
hasAudioFocus = false
Timber.tag("PlayerService").i("Audio focus abandoned on idle")
}
stopServiceGracefully()
}
}
Player.STATE_BUFFERING -> {
Timber.tag("PlayerService").i("Playback state: BUFFERING")
handleBufferingState()
}
}
} catch (e: Exception) {
Timber.tag("PlayerService").e("Error in onPlaybackStateChanged: ${e.message}")
}
}
override fun onPlayerError(error: PlaybackException) {
Timber.tag("PlayerService").e("Playback error: ${error.errorCodeName} - ${error.message}")
when (error.errorCode) {
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED -> {
Timber.tag("PlayerService").e("Network connection failed")
handlePlaybackError(error)
}
PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS -> {
Timber.tag("PlayerService").e("Bad HTTP status: ${error.message}")
handlePlaybackError(error)
}
PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE -> {
Timber.tag("PlayerService").e("Invalid HTTP content type")
handlePlaybackError(error)
}
PlaybackException.ERROR_CODE_TIMEOUT -> {
Timber.tag("PlayerService").e("Playback timeout")
handlePlaybackError(error)
}
PlaybackException.ERROR_CODE_IO_UNSPECIFIED -> {
Timber.tag("PlayerService").e("Unspecified IO error")
handlePlaybackError(error)
}
PlaybackException.ERROR_CODE_PARSING_CONTAINER_MALFORMED,
PlaybackException.ERROR_CODE_PARSING_MANIFEST_MALFORMED -> {
Timber.tag("PlayerService").e("Malformed stream format")
stopServiceGracefully()
}
else -> {
Timber.tag("PlayerService").e("Unknown playback error: ${error.errorCode}")
handlePlaybackError(error)
}
}
}
override fun onMetadata(metadata: Metadata) {
super.onMetadata(metadata)
try {
if (metadata.length() > 0) {
val metadataString = AudioHelper.getMetadataString(metadata)
if (metadataString.isNotEmpty()) {
Timber.tag("PlayerService").i("Metadata received: $metadataString")
// Сохраняем метаданные в историю
val metadataHistory = PreferencesHelper.loadMetadataHistory()
// Добавляем только если это новые метаданные
if (metadataHistory.isEmpty() || metadataHistory.last() != metadataString) {
metadataHistory.add(metadataString)
// Ограничиваем размер истории
while (metadataHistory.size > Keys.DEFAULT_SIZE_OF_METADATA_HISTORY) {
metadataHistory.removeAt(0)
}
PreferencesHelper.saveMetadataHistory(metadataHistory)
Timber.tag("PlayerService").i("Metadata saved to history")
}
}
}
} catch (e: Exception) {
Timber.tag("PlayerService").e("Error processing metadata: ${e.message}")
}
}
private fun handlePlaybackReady() {
optimizePowerUsage()
}
private fun handleBufferingState() {
exoPlayer?.playWhenReady = true
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
getString(R.string.notification_info),
NotificationManager.IMPORTANCE_LOW
).apply {
description = getString(R.string.default_notification_channel_name)
setShowBadge(false)
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
enableLights(false)
enableVibration(false)
setSound(null, null)
}
notificationManager.createNotificationChannel(channel)
}
}
private fun createInitialNotification(): Notification {
return NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
.setContentTitle(getString(R.string.app_name))
.setContentText(getString(R.string.notification_info))
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentIntent(getSessionActivity())
.setPriority(NotificationCompat.PRIORITY_LOW)
.setOngoing(true)
.build()
}
fun playRadio(url: String, title: String, artist: String, artUri: Uri?, stationUuid: String? = null) {
try {
Timber.tag("PlayerService").i("=== playRadio called ===")
Timber.tag("PlayerService").i("Title: $title, URL: $url, UUID: $stationUuid")
if (url.isBlank()) {
Timber.tag("PlayerService").e("Invalid URL: URL is blank")
return
}
exoPlayer?.apply {
// Применяем индивидуальную громкость ДО начала воспроизведения
val targetVolume = if (!stationUuid.isNullOrBlank()) {
val stationVolume = VolumeSettingsHelper.getVolume(this@PlayerService, stationUuid)
Timber.tag("PlayerService").i("Station has custom volume: $stationVolume (${(stationVolume * 100).toInt()}%)")
stationVolume
} else {
Timber.tag("PlayerService").i("No UUID provided, using default volume: 1.0")
1.0f
}
// Устанавливаем громкость перед подготовкой
volume = targetVolume
Timber.tag("PlayerService").i("Volume set to: $targetVolume before playback")
// Сбрасываем флаги Audio Focus для нового воспроизведения
resumeOnFocusGain = false
playbackDelayed = false
// Останавливаем текущее воспроизведение
stop()
clearMediaItems()
val mediaItem = MediaItem.Builder()
.setUri(url)
.setMediaMetadata(
MediaMetadata.Builder()
.setTitle(title)
.setArtist(artist)
.setArtworkUri(artUri)
.build()
)
.build()
setMediaItem(mediaItem)
// Подтверждаем, что громкость установлена
Timber.tag("PlayerService").i("Current player volume before prepare: $volume")
// Запрашиваем Audio Focus ПЕРЕД prepare и play
requestAudioFocusAndPlay()
} ?: run {
Timber.tag("PlayerService").e("ExoPlayer is null, cannot play radio")
}
} catch (e: IllegalArgumentException) {
Timber.tag("PlayerService").e("Invalid URL format: ${e.message}")
} catch (e: IllegalStateException) {
Timber.tag("PlayerService").e("Player in invalid state: ${e.message}")
} catch (e: Exception) {
Timber.tag("PlayerService").e("Error playing radio: ${e.message}")
handlePlaybackError(PlaybackException("Error playing radio", e, PlaybackException.ERROR_CODE_UNSPECIFIED))
}
}
private fun requestAudioFocusAndPlay() {
Timber.tag("PlayerService").i("Requesting audio focus...")
val result = audioManager.requestAudioFocus(
audioFocusRequest,
AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN
)
Timber.tag("PlayerService").i("Audio focus request result: $result")
when (result) {
AudioManager.AUDIOFOCUS_REQUEST_GRANTED -> {
Timber.tag("PlayerService").i("Audio focus GRANTED - starting playback")
hasAudioFocus = true
playbackDelayed = false
// Теперь можем безопасно начать воспроизведение
exoPlayer?.let { player ->
player.prepare()
player.play()
Timber.tag("PlayerService").i("Playback started with audio focus")
}
}
AudioManager.AUDIOFOCUS_REQUEST_FAILED -> {
Timber.tag("PlayerService").w("Audio focus FAILED - cannot play")
hasAudioFocus = false
playbackDelayed = false
Toast.makeText(this, getString(R.string.notification_play), Toast.LENGTH_SHORT).show()
}
AudioManager.AUDIOFOCUS_REQUEST_DELAYED -> {
Timber.tag("PlayerService").i("Audio focus DELAYED - will play when granted")
hasAudioFocus = false
playbackDelayed = true
// prepare() вызовем сейчас, play() будет вызван в onAudioFocusChange
exoPlayer?.prepare()
}
}
}
private fun stopServiceGracefully() {
Timber.tag("PlayerService").i("Stopping service gracefully")
// Освобождаем Audio Focus перед остановкой
if (hasAudioFocus) {
audioManager.abandonAudioFocus(audioFocusRequest)
hasAudioFocus = false
Timber.tag("PlayerService").i("Audio focus abandoned on stop")
}
exoPlayer?.stop()
stopForeground(true)
stopSelf()
}
private fun optimizePowerUsage() {
exoPlayer?.setWakeMode(
if (isCharging(this)) C.WAKE_MODE_NETWORK
else C.WAKE_MODE_LOCAL
)
}
private fun handlePlaybackEnd() {
stopServiceGracefully()
}
private fun isAppInForeground(): Boolean {
val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val appProcesses = activityManager.runningAppProcesses ?: return false
val packageName = packageName
return appProcesses.any { process ->
process.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND &&
process.processName == packageName
}
}
fun isCharging(context: Context): Boolean {
val intentFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
val batteryStatus: Intent? = context.registerReceiver(null, intentFilter)
val status = batteryStatus?.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
return status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL
}
private var retryAttempts = 0
private val maxRetryAttempts = 3
private val retryHandler = Handler(Looper.getMainLooper())
private fun handlePlaybackError(error: PlaybackException) {
Timber.tag("PlayerService").e("Playback error: ${error.errorCodeName} - ${error.message}")
when (error.errorCode) {
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
PlaybackException.ERROR_CODE_TIMEOUT,
PlaybackException.ERROR_CODE_IO_UNSPECIFIED -> {
if (retryAttempts < maxRetryAttempts) {
retryPlayback()
} else {
Timber.tag("PlayerService").e("Max retry attempts reached, stopping playback")
retryAttempts = 0
stopServiceGracefully()
}
}
else -> {
retryAttempts = 0
stopServiceGracefully()
}
}
}
private fun retryPlayback() {
retryAttempts++
val delayMillis = (retryAttempts * 2000L) // Exponential backoff: 2s, 4s, 6s
Timber.tag("PlayerService").i("Retrying playback (attempt $retryAttempts/$maxRetryAttempts) in ${delayMillis}ms")
retryHandler.postDelayed({
try {
exoPlayer?.let { player ->
if (player.playbackState == Player.STATE_IDLE ||
player.playbackState == Player.STATE_ENDED) {
player.prepare()
player.play()
}
}
} catch (e: Exception) {
Timber.tag("PlayerService").e("Error during retry: ${e.message}")
retryAttempts = 0
stopServiceGracefully()
}
}, delayMillis)
}
private inner class NotificationMediaAdapter : PlayerNotificationManager.MediaDescriptionAdapter {
override fun getCurrentContentTitle(player: Player): CharSequence {
return player.mediaMetadata.title ?: getString(R.string.app_name)
}
override fun createCurrentContentIntent(player: Player): PendingIntent? {
return getSessionActivity()
}
override fun getCurrentContentText(player: Player): CharSequence? {
return player.mediaMetadata.artist ?: getString(R.string.sample_text_station_metadata)
}
override fun getCurrentLargeIcon(
player: Player,
callback: PlayerNotificationManager.BitmapCallback
): Bitmap? {
val imageUri = player.mediaMetadata.artworkUri
return if (imageUri != null) {
loadBitmapAsync(imageUri.toString(), callback)
null
} else {
null
}
}
private fun loadBitmapAsync(url: String, callback: PlayerNotificationManager.BitmapCallback) {
CoroutineScope(Dispatchers.IO).launch {
try {
val bitmap = Glide.with(this@PlayerService)
.asBitmap()
.load(url)
.apply(RequestOptions().timeout(10000))
.submit()
.get()
callback.onBitmap(bitmap)
} catch (e: Exception) {
Timber.tag("Player").v("Playback error: notification")
}
}
}
}
companion object {
const val NOTIFICATION_ID = 2031
const val NOTIFICATION_CHANNEL_ID = "Проигрование"
private const val MIN_BUFFER_MS = 5000
private const val MAX_BUFFER_MS = 10000
private const val PLAYBACK_BUFFER_MS = 2000
private const val REBUFFER_MS = 5000
}
}