Merge pull request #990 from Bnyro/notification

Notification and autoplay refactor
This commit is contained in:
Bnyro 2022-08-07 20:02:36 +02:00 committed by GitHub
commit 79de2ab9d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 283 additions and 279 deletions

View File

@ -1,7 +1,6 @@
package com.github.libretube.fragments package com.github.libretube.fragments
import android.app.ActivityManager import android.app.ActivityManager
import android.app.NotificationManager
import android.app.PictureInPictureParams import android.app.PictureInPictureParams
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@ -16,7 +15,6 @@ import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.os.PowerManager import android.os.PowerManager
import android.support.v4.media.session.MediaSessionCompat
import android.text.Html import android.text.Html
import android.text.format.DateUtils import android.text.format.DateUtils
import android.util.Log import android.util.Log
@ -35,9 +33,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import com.github.libretube.BACKGROUND_CHANNEL_ID
import com.github.libretube.Globals import com.github.libretube.Globals
import com.github.libretube.PLAYER_NOTIFICATION_ID
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.activities.MainActivity import com.github.libretube.activities.MainActivity
import com.github.libretube.adapters.ChaptersAdapter import com.github.libretube.adapters.ChaptersAdapter
@ -50,17 +46,17 @@ import com.github.libretube.dialogs.AddToPlaylistDialog
import com.github.libretube.dialogs.DownloadDialog import com.github.libretube.dialogs.DownloadDialog
import com.github.libretube.dialogs.ShareDialog import com.github.libretube.dialogs.ShareDialog
import com.github.libretube.obj.ChapterSegment import com.github.libretube.obj.ChapterSegment
import com.github.libretube.obj.Playlist
import com.github.libretube.obj.Segment import com.github.libretube.obj.Segment
import com.github.libretube.obj.Segments import com.github.libretube.obj.Segments
import com.github.libretube.obj.Streams import com.github.libretube.obj.Streams
import com.github.libretube.preferences.PreferenceHelper import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.preferences.PreferenceKeys import com.github.libretube.preferences.PreferenceKeys
import com.github.libretube.services.BackgroundMode import com.github.libretube.services.BackgroundMode
import com.github.libretube.util.AutoPlayHelper
import com.github.libretube.util.BackgroundHelper import com.github.libretube.util.BackgroundHelper
import com.github.libretube.util.ConnectionHelper import com.github.libretube.util.ConnectionHelper
import com.github.libretube.util.CronetHelper import com.github.libretube.util.CronetHelper
import com.github.libretube.util.DescriptionAdapter import com.github.libretube.util.NowPlayingNotification
import com.github.libretube.util.OnDoubleTapEventListener import com.github.libretube.util.OnDoubleTapEventListener
import com.github.libretube.util.PlayerHelper import com.github.libretube.util.PlayerHelper
import com.github.libretube.util.RetrofitInstance import com.github.libretube.util.RetrofitInstance
@ -77,7 +73,6 @@ import com.google.android.exoplayer2.MediaItem.fromUri
import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.audio.AudioAttributes import com.google.android.exoplayer2.audio.AudioAttributes
import com.google.android.exoplayer2.ext.cronet.CronetDataSource import com.google.android.exoplayer2.ext.cronet.CronetDataSource
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory import com.google.android.exoplayer2.source.DefaultMediaSourceFactory
import com.google.android.exoplayer2.source.MediaSource import com.google.android.exoplayer2.source.MediaSource
import com.google.android.exoplayer2.source.MergingMediaSource import com.google.android.exoplayer2.source.MergingMediaSource
@ -85,7 +80,6 @@ import com.google.android.exoplayer2.source.ProgressiveMediaSource
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
import com.google.android.exoplayer2.ui.CaptionStyleCompat import com.google.android.exoplayer2.ui.CaptionStyleCompat
import com.google.android.exoplayer2.ui.PlayerNotificationManager
import com.google.android.exoplayer2.ui.StyledPlayerView import com.google.android.exoplayer2.ui.StyledPlayerView
import com.google.android.exoplayer2.ui.TimeBar import com.google.android.exoplayer2.ui.TimeBar
import com.google.android.exoplayer2.upstream.DataSource import com.google.android.exoplayer2.upstream.DataSource
@ -175,15 +169,12 @@ class PlayerFragment : Fragment() {
* for autoplay * for autoplay
*/ */
private var nextStreamId: String? = null private var nextStreamId: String? = null
private var playlistStreamIds: MutableList<String> = arrayListOf() private lateinit var autoPlayHelper: AutoPlayHelper
private var playlistNextPage: String? = null
/** /**
* for the player notification * for the player notification
*/ */
private lateinit var mediaSession: MediaSessionCompat private lateinit var nowPlayingNotification: NowPlayingNotification
private lateinit var mediaSessionConnector: MediaSessionConnector
private lateinit var playerNotification: PlayerNotificationManager
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -676,15 +667,7 @@ class PlayerFragment : Fragment() {
super.onDestroy() super.onDestroy()
try { try {
saveWatchPosition() saveWatchPosition()
mediaSession.isActive = false nowPlayingNotification.destroy()
mediaSession.release()
mediaSessionConnector.setPlayer(null)
playerNotification.setPlayer(null)
val notificationManager = context?.getSystemService(
Context.NOTIFICATION_SERVICE
) as NotificationManager
notificationManager.cancel(1)
exoPlayer.release()
activity?.requestedOrientation = activity?.requestedOrientation =
if ((activity as MainActivity).autoRotationEnabled) ActivityInfo.SCREEN_ORIENTATION_USER if ((activity as MainActivity).autoRotationEnabled) ActivityInfo.SCREEN_ORIENTATION_USER
else ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT else ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
@ -752,12 +735,12 @@ class PlayerFragment : Fragment() {
exoPlayer.prepare() exoPlayer.prepare()
exoPlayer.play() exoPlayer.play()
exoPlayerView.useController = true exoPlayerView.useController = true
initializePlayerNotification(requireContext()) initializePlayerNotification()
if (sponsorBlockEnabled) fetchSponsorBlockSegments() if (sponsorBlockEnabled) fetchSponsorBlockSegments()
// show comments if related streams disabled // show comments if related streams disabled
if (!relatedStreamsEnabled) toggleComments() if (!relatedStreamsEnabled) toggleComments()
// prepare for autoplay // prepare for autoplay
initAutoPlay() if (autoplayEnabled) setNextStream()
if (watchHistoryEnabled) { if (watchHistoryEnabled) {
PreferenceHelper.addToWatchHistory(videoId!!, streams) PreferenceHelper.addToWatchHistory(videoId!!, streams)
} }
@ -767,6 +750,20 @@ class PlayerFragment : Fragment() {
run() run()
} }
/**
* set the videoId of the next stream for autoplay
*/
private fun setNextStream() {
nextStreamId = streams.relatedStreams!![0].url.toID()
if (playlistId == null) return
if (!this::autoPlayHelper.isInitialized) autoPlayHelper = AutoPlayHelper(playlistId!!)
// search for the next videoId in the playlist
lifecycleScope.launchWhenCreated {
val nextId = autoPlayHelper.getNextPlaylistVideoId(videoId!!)
if (nextId != null) nextStreamId = nextId
}
}
/** /**
* fetch the segments for SponsorBlock * fetch the segments for SponsorBlock
*/ */
@ -824,59 +821,6 @@ class PlayerFragment : Fragment() {
if (position != null) exoPlayer.seekTo(position!!) if (position != null) exoPlayer.seekTo(position!!)
} }
// the function is working recursively
private fun initAutoPlay() {
// save related streams for autoplay
if (autoplayEnabled) {
// if it's a playlist use the next video
if (playlistId != null) {
lateinit var playlist: Playlist // var for saving the list in
// runs only the first time when starting a video from a playlist
if (playlistStreamIds.isEmpty()) {
CoroutineScope(Dispatchers.IO).launch {
// fetch the playlists videos
playlist = RetrofitInstance.api.getPlaylist(playlistId!!)
// save the playlist urls in the array
playlist.relatedStreams?.forEach { video ->
playlistStreamIds += video.url.toID()
}
// save playlistNextPage for usage if video is not contained
playlistNextPage = playlist.nextpage
// restart the function after videos are loaded
initAutoPlay()
}
}
// if the playlists contain the video, then save the next video as next stream
else if (playlistStreamIds.contains(videoId)) {
val index = playlistStreamIds.indexOf(videoId)
// check whether there's a next video
if (index + 1 <= playlistStreamIds.size) {
nextStreamId = playlistStreamIds[index + 1]
}
// fetch the next page of the playlist if the video isn't contained
} else if (playlistNextPage != null) {
CoroutineScope(Dispatchers.IO).launch {
RetrofitInstance.api.getPlaylistNextPage(playlistId!!, playlistNextPage!!)
// append all the playlist item urls to the array
playlist.relatedStreams?.forEach { video ->
playlistStreamIds += video.url.toID()
}
// save playlistNextPage for usage if video is not contained
playlistNextPage = playlist.nextpage
// restart the function after videos are loaded
initAutoPlay()
}
}
// else: the video must be the last video of the playlist so nothing happens
// if it's not a playlist then use the next related video
} else if (streams.relatedStreams != null && streams.relatedStreams!!.isNotEmpty()) {
// save next video from related streams for autoplay
nextStreamId = streams.relatedStreams!![0].url.toID()
}
}
}
// used for autoplay and skipping to next video // used for autoplay and skipping to next video
private fun playNextVideo() { private fun playNextVideo() {
// check whether there is a new video in the queue // check whether there is a new video in the queue
@ -1502,33 +1446,14 @@ class PlayerFragment : Fragment() {
exoPlayer.setAudioAttributes(audioAttributes, true) exoPlayer.setAudioAttributes(audioAttributes, true)
} }
private fun initializePlayerNotification(c: Context) { /**
mediaSession = MediaSessionCompat(c, this.javaClass.name) * show the [NowPlayingNotification] for the current video
mediaSession.apply { */
isActive = true private fun initializePlayerNotification() {
} if (!this::nowPlayingNotification.isInitialized) {
nowPlayingNotification = NowPlayingNotification(requireContext(), exoPlayer)
mediaSessionConnector = MediaSessionConnector(mediaSession)
mediaSessionConnector.setPlayer(exoPlayer)
playerNotification = PlayerNotificationManager
.Builder(c, PLAYER_NOTIFICATION_ID, BACKGROUND_CHANNEL_ID)
.setMediaDescriptionAdapter(
DescriptionAdapter(
streams.title!!,
streams.uploader!!,
streams.thumbnailUrl!!,
requireContext()
)
)
.build()
playerNotification.apply {
setPlayer(exoPlayer)
setUsePreviousAction(false)
setUseStopAction(true)
setMediaSessionToken(mediaSession.sessionToken)
} }
nowPlayingNotification.updatePlayerNotification(streams)
} }
// lock the player // lock the player

View File

@ -4,13 +4,11 @@ import android.app.Notification
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.Service import android.app.Service
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.IBinder import android.os.IBinder
import android.os.Looper import android.os.Looper
import android.support.v4.media.session.MediaSessionCompat
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import com.github.libretube.BACKGROUND_CHANNEL_ID import com.github.libretube.BACKGROUND_CHANNEL_ID
import com.github.libretube.PLAYER_NOTIFICATION_ID import com.github.libretube.PLAYER_NOTIFICATION_ID
@ -20,7 +18,7 @@ import com.github.libretube.obj.Segments
import com.github.libretube.obj.Streams import com.github.libretube.obj.Streams
import com.github.libretube.preferences.PreferenceHelper import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.preferences.PreferenceKeys import com.github.libretube.preferences.PreferenceKeys
import com.github.libretube.util.DescriptionAdapter import com.github.libretube.util.NowPlayingNotification
import com.github.libretube.util.PlayerHelper import com.github.libretube.util.PlayerHelper
import com.github.libretube.util.RetrofitInstance import com.github.libretube.util.RetrofitInstance
import com.github.libretube.util.toID import com.github.libretube.util.toID
@ -30,7 +28,6 @@ import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.audio.AudioAttributes import com.google.android.exoplayer2.audio.AudioAttributes
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
import com.google.android.exoplayer2.ui.PlayerNotificationManager
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -56,21 +53,6 @@ class BackgroundMode : Service() {
private var player: ExoPlayer? = null private var player: ExoPlayer? = null
private var playWhenReadyPlayer = true private var playWhenReadyPlayer = true
/**
* The [MediaSessionCompat] for the [response].
*/
private lateinit var mediaSession: MediaSessionCompat
/**
* The [MediaSessionConnector] to connect with the [mediaSession] and implement it with the [player].
*/
private lateinit var mediaSessionConnector: MediaSessionConnector
/**
* The [PlayerNotificationManager] to load the [mediaSession] content on it.
*/
private var playerNotification: PlayerNotificationManager? = null
/** /**
* The [AudioAttributes] handle the audio focus of the [player] * The [AudioAttributes] handle the audio focus of the [player]
*/ */
@ -81,11 +63,16 @@ class BackgroundMode : Service() {
*/ */
private var segmentData: Segments? = null private var segmentData: Segments? = null
/**
* Notification for the player
*/
private lateinit var nowPlayingNotification: NowPlayingNotification
/**
* Setting the required [notification] for running as a foreground service
*/
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
/**
* setting the required notification for running as a foreground service
*/
if (Build.VERSION.SDK_INT >= 26) { if (Build.VERSION.SDK_INT >= 26) {
val channelId = BACKGROUND_CHANNEL_ID val channelId = BACKGROUND_CHANNEL_ID
val channel = NotificationChannel( val channel = NotificationChannel(
@ -106,15 +93,17 @@ class BackgroundMode : Service() {
* Initializes the [player] with the [MediaItem]. * Initializes the [player] with the [MediaItem].
*/ */
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// destroy the old player try {
destroyPlayer() // get the intent arguments
videoId = intent?.getStringExtra("videoId")!!
val position = intent.getLongExtra("position", 0L)
// get the intent arguments // play the audio in the background
videoId = intent?.getStringExtra("videoId")!! playAudio(videoId, position)
val position = intent.getLongExtra("position", 0L) } catch (e: Exception) {
stopForeground(true)
// play the audio in the background stopSelf()
playAudio(videoId, position) }
return super.onStartCommand(intent, flags, startId) return super.onStartCommand(intent, flags, startId)
} }
@ -133,7 +122,13 @@ class BackgroundMode : Service() {
job.join() job.join()
initializePlayer() initializePlayer()
initializePlayerNotification() setMediaItem()
// create the notification
if (!this@BackgroundMode::nowPlayingNotification.isInitialized) {
nowPlayingNotification = NowPlayingNotification(this@BackgroundMode, player!!)
}
nowPlayingNotification.updatePlayerNotification(response!!)
player?.apply { player?.apply {
playWhenReady = playWhenReadyPlayer playWhenReady = playWhenReadyPlayer
@ -174,16 +169,11 @@ class BackgroundMode : Service() {
if (autoplay) playNextVideo() if (autoplay) playNextVideo()
} }
Player.STATE_IDLE -> { Player.STATE_IDLE -> {
// called when the user pressed stop in the notification onDestroy()
// stop the service from being in the foreground and remove the notification
stopForeground(true)
// destroy the service
stopSelf()
} }
} }
} }
}) })
setMediaItem()
} }
/** /**
@ -194,9 +184,6 @@ class BackgroundMode : Service() {
val videoId = response!! val videoId = response!!
.relatedStreams!![0].url.toID() .relatedStreams!![0].url.toID()
// destroy previous notification and player
destroyPlayer()
// play new video on background // play new video on background
this.videoId = videoId this.videoId = videoId
this.segmentData = null this.segmentData = null
@ -204,32 +191,6 @@ class BackgroundMode : Service() {
} }
} }
/**
* Initializes the [playerNotification] attached to the [player] and shows it.
*/
private fun initializePlayerNotification() {
playerNotification = PlayerNotificationManager
.Builder(this, PLAYER_NOTIFICATION_ID, BACKGROUND_CHANNEL_ID)
// set the description of the notification
.setMediaDescriptionAdapter(
DescriptionAdapter(
response?.title!!,
response?.uploader!!,
response?.thumbnailUrl!!,
this
)
)
.build()
playerNotification?.apply {
setPlayer(player)
setUseNextAction(false)
setUsePreviousAction(false)
setUseStopAction(true)
setColorized(true)
setMediaSessionToken(mediaSession.sessionToken)
}
}
/** /**
* Sets the [MediaItem] with the [response] into the [player]. Also creates a [MediaSessionConnector] * Sets the [MediaItem] with the [response] into the [player]. Also creates a [MediaSessionConnector]
* with the [mediaSession] and attach it to the [player]. * with the [mediaSession] and attach it to the [player].
@ -239,12 +200,6 @@ class BackgroundMode : Service() {
val mediaItem = MediaItem.Builder().setUri(it.hls!!).build() val mediaItem = MediaItem.Builder().setUri(it.hls!!).build()
player?.setMediaItem(mediaItem) player?.setMediaItem(mediaItem)
} }
mediaSession = MediaSessionCompat(this, this.javaClass.name)
mediaSession.isActive = true
mediaSessionConnector = MediaSessionConnector(mediaSession)
mediaSessionConnector.setPlayer(player)
} }
/** /**
@ -284,15 +239,17 @@ class BackgroundMode : Service() {
} }
} }
private fun destroyPlayer() { /**
// clear old player and its notification * destroy the [BackgroundMode] foreground service
playerNotification = null */
player = null override fun onDestroy() {
// called when the user pressed stop in the notification
// kill old notification // stop the service from being in the foreground and remove the notification
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) stopForeground(true)
as NotificationManager // destroy the service
notificationManager.cancel(PLAYER_NOTIFICATION_ID) stopSelf()
if (this::nowPlayingNotification.isInitialized) nowPlayingNotification.destroy()
super.onDestroy()
} }
override fun onBind(p0: Intent?): IBinder? { override fun onBind(p0: Intent?): IBinder? {

View File

@ -0,0 +1,37 @@
package com.github.libretube.util
import com.github.libretube.obj.Playlist
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class AutoPlayHelper(
private val playlistId: String
) {
private val TAG = "AutoPlayHelper"
private val playlistStreamIds = mutableListOf<String>()
private lateinit var playlist: Playlist
private var playlistNextPage: String? = null
suspend fun getNextPlaylistVideoId(currentVideoId: String): String? {
// if the playlists contain the video, then save the next video as next stream
if (playlistStreamIds.contains(currentVideoId)) {
val index = playlistStreamIds.indexOf(currentVideoId)
// check whether there's a next video
return if (index < playlistStreamIds.size) playlistStreamIds[index + 1]
else getNextPlaylistVideoId(currentVideoId)
} else if (playlistStreamIds.isEmpty() || playlistNextPage != null) {
// fetch the next page of the playlist
return withContext(Dispatchers.IO) {
// fetch the playlists videos
playlist = RetrofitInstance.api.getPlaylist(playlistId)
// save the playlist urls in the array
playlistStreamIds += playlist.relatedStreams!!.map { it.url.toID() }
// save playlistNextPage for usage if video is not contained
playlistNextPage = playlist.nextpage
return@withContext getNextPlaylistVideoId(currentVideoId)
}
}
return null
}
}

View File

@ -1,95 +0,0 @@
package com.github.libretube.util
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import com.github.libretube.activities.MainActivity
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ui.PlayerNotificationManager
import java.net.URL
/**
* The [DescriptionAdapter] is used to show title, uploaderName and thumbnail of the video in the notification
* Basic example [here](https://github.com/AnthonyMarkD/AudioPlayerSampleTest)
*/
class DescriptionAdapter(
private val title: String,
private val channelName: String,
private val thumbnailUrl: String,
private val context: Context
) :
PlayerNotificationManager.MediaDescriptionAdapter {
/**
* sets the title of the notification
*/
override fun getCurrentContentTitle(player: Player): CharSequence {
// return controller.metadata.description.title.toString()
return title
}
/**
* overrides the action when clicking the notification
*/
override fun createCurrentContentIntent(player: Player): PendingIntent? {
// return controller.sessionActivity
/**
* starts a new MainActivity Intent when the player notification is clicked
* it doesn't start a completely new MainActivity because the MainActivity's launchMode
* is set to "singleTop" in the AndroidManifest (important!!!)
* that's the only way to launch back into the previous activity (e.g. the player view
*/
val intent = Intent(context, MainActivity::class.java)
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
}
/**
* the description of the notification (below the title)
*/
override fun getCurrentContentText(player: Player): CharSequence? {
// return controller.metadata.description.subtitle.toString()
return channelName
}
/**
* return the icon/thumbnail of the video
*/
override fun getCurrentLargeIcon(
player: Player,
callback: PlayerNotificationManager.BitmapCallback
): Bitmap? {
lateinit var bitmap: Bitmap
/**
* running on a new thread to prevent a NetworkMainThreadException
*/
val thread = Thread {
try {
/**
* try to GET the thumbnail from the URL
*/
val inputStream = URL(thumbnailUrl).openStream()
bitmap = BitmapFactory.decodeStream(inputStream)
} catch (ex: java.lang.Exception) {
ex.printStackTrace()
}
}
thread.start()
thread.join()
/**
* returns the scaled bitmap if it got fetched successfully
*/
return try {
val resizedBitmap = Bitmap.createScaledBitmap(
bitmap,
bitmap.width,
bitmap.width,
false
)
resizedBitmap
} catch (e: Exception) {
null
}
}
}

View File

@ -0,0 +1,180 @@
package com.github.libretube.util
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.support.v4.media.session.MediaSessionCompat
import com.github.libretube.BACKGROUND_CHANNEL_ID
import com.github.libretube.PLAYER_NOTIFICATION_ID
import com.github.libretube.activities.MainActivity
import com.github.libretube.obj.Streams
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
import com.google.android.exoplayer2.ui.PlayerNotificationManager
import java.net.URL
class NowPlayingNotification(
private val context: Context,
private val player: ExoPlayer
) {
private var streams: Streams? = null
/**
* The [MediaSessionCompat] for the [streams].
*/
private lateinit var mediaSession: MediaSessionCompat
/**
* The [MediaSessionConnector] to connect with the [mediaSession] and implement it with the [player].
*/
private lateinit var mediaSessionConnector: MediaSessionConnector
/**
* The [PlayerNotificationManager] to load the [mediaSession] content on it.
*/
private var playerNotification: PlayerNotificationManager? = null
/**
* The [DescriptionAdapter] is used to show title, uploaderName and thumbnail of the video in the notification
* Basic example [here](https://github.com/AnthonyMarkD/AudioPlayerSampleTest)
*/
inner class DescriptionAdapter() :
PlayerNotificationManager.MediaDescriptionAdapter {
/**
* sets the title of the notification
*/
override fun getCurrentContentTitle(player: Player): CharSequence {
// return controller.metadata.description.title.toString()
return streams?.title!!
}
/**
* overrides the action when clicking the notification
*/
override fun createCurrentContentIntent(player: Player): PendingIntent? {
// return controller.sessionActivity
/**
* starts a new MainActivity Intent when the player notification is clicked
* it doesn't start a completely new MainActivity because the MainActivity's launchMode
* is set to "singleTop" in the AndroidManifest (important!!!)
* that's the only way to launch back into the previous activity (e.g. the player view
*/
val intent = Intent(context, MainActivity::class.java)
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
}
/**
* the description of the notification (below the title)
*/
override fun getCurrentContentText(player: Player): CharSequence? {
// return controller.metadata.description.subtitle.toString()
return streams?.uploader
}
/**
* return the icon/thumbnail of the video
*/
override fun getCurrentLargeIcon(
player: Player,
callback: PlayerNotificationManager.BitmapCallback
): Bitmap? {
lateinit var bitmap: Bitmap
/**
* running on a new thread to prevent a NetworkMainThreadException
*/
val thread = Thread {
try {
/**
* try to GET the thumbnail from the URL
*/
val inputStream = URL(streams?.thumbnailUrl).openStream()
bitmap = BitmapFactory.decodeStream(inputStream)
} catch (ex: java.lang.Exception) {
ex.printStackTrace()
}
}
thread.start()
thread.join()
/**
* returns the scaled bitmap if it got fetched successfully
*/
return try {
val resizedBitmap = Bitmap.createScaledBitmap(
bitmap,
bitmap.width,
bitmap.width,
false
)
resizedBitmap
} catch (e: Exception) {
null
}
}
}
/**
* Creates a [MediaSessionCompat] amd a [MediaSessionConnector] for the player
*/
private fun createMediaSession() {
if (this::mediaSession.isInitialized) return
mediaSession = MediaSessionCompat(context, this.javaClass.name)
mediaSession.isActive = true
mediaSessionConnector = MediaSessionConnector(mediaSession)
mediaSessionConnector.setPlayer(player)
}
/**
* Updates or creates the [playerNotification]
*/
fun updatePlayerNotification(
streams: Streams
) {
this.streams = streams
if (playerNotification == null) {
createMediaSession()
createNotification()
}
}
/**
* Initializes the [playerNotification] attached to the [player] and shows it.
*/
private fun createNotification() {
playerNotification = PlayerNotificationManager
.Builder(context, PLAYER_NOTIFICATION_ID, BACKGROUND_CHANNEL_ID)
// set the description of the notification
.setMediaDescriptionAdapter(
DescriptionAdapter()
)
.build()
playerNotification?.apply {
setPlayer(player)
setUseNextAction(false)
setUsePreviousAction(false)
setUseStopAction(true)
setColorized(true)
setMediaSessionToken(mediaSession.sessionToken)
}
}
/**
* Destroy the [NowPlayingNotification]
*/
fun destroy() {
mediaSession.isActive = false
mediaSession.release()
mediaSessionConnector.setPlayer(null)
playerNotification?.setPlayer(null)
val notificationManager = context.getSystemService(
Context.NOTIFICATION_SERVICE
) as NotificationManager
notificationManager.cancel(PLAYER_NOTIFICATION_ID)
player.release()
}
}