Audio/background player for downloads

This commit is contained in:
Bnyro 2023-04-10 15:54:09 +02:00
parent 57b7dfdda9
commit dcdd1af176
12 changed files with 277 additions and 80 deletions

View File

@ -339,7 +339,12 @@
android:exported="false" /> android:exported="false" />
<service <service
android:name=".services.BackgroundMode" android:name=".services.OnlinePlayerService"
android:enabled="true"
android:exported="false" />
<service
android:name=".services.OfflinePlayerService"
android:enabled="true" android:enabled="true"
android:exported="false" /> android:exported="false" />

View File

@ -7,17 +7,17 @@ import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.fragment.app.commit import androidx.fragment.app.commit
import com.github.libretube.constants.IntentData import com.github.libretube.constants.IntentData
import com.github.libretube.services.BackgroundMode import com.github.libretube.services.OnlinePlayerService
import com.github.libretube.ui.activities.MainActivity import com.github.libretube.ui.activities.MainActivity
import com.github.libretube.ui.fragments.PlayerFragment import com.github.libretube.ui.fragments.PlayerFragment
/** /**
* Helper for starting a new Instance of the [BackgroundMode] * Helper for starting a new Instance of the [OnlinePlayerService]
*/ */
object BackgroundHelper { object BackgroundHelper {
/** /**
* Start the foreground service [BackgroundMode] to play in background. [position] * Start the foreground service [OnlinePlayerService] to play in background. [position]
* is seek to position specified in milliseconds in the current [videoId]. * is seek to position specified in milliseconds in the current [videoId].
*/ */
fun playOnBackground( fun playOnBackground(
@ -39,7 +39,7 @@ object BackgroundHelper {
} }
// create an intent for the background mode service // create an intent for the background mode service
val intent = Intent(context, BackgroundMode::class.java) val intent = Intent(context, OnlinePlayerService::class.java)
intent.putExtra(IntentData.videoId, videoId) intent.putExtra(IntentData.videoId, videoId)
intent.putExtra(IntentData.playlistId, playlistId) intent.putExtra(IntentData.playlistId, playlistId)
intent.putExtra(IntentData.channelId, channelId) intent.putExtra(IntentData.channelId, channelId)
@ -51,22 +51,22 @@ object BackgroundHelper {
} }
/** /**
* Stop the [BackgroundMode] service if it is running. * Stop the [OnlinePlayerService] service if it is running.
*/ */
fun stopBackgroundPlay(context: Context) { fun stopBackgroundPlay(context: Context) {
if (isBackgroundServiceRunning(context)) { if (isBackgroundServiceRunning(context)) {
// Intent to stop background mode service // Intent to stop background mode service
val intent = Intent(context, BackgroundMode::class.java) val intent = Intent(context, OnlinePlayerService::class.java)
context.stopService(intent) context.stopService(intent)
} }
} }
/** /**
* Check if the [BackgroundMode] service is currently running. * Check if the [OnlinePlayerService] service is currently running.
*/ */
fun isBackgroundServiceRunning(context: Context): Boolean { fun isBackgroundServiceRunning(context: Context): Boolean {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
return context.getSystemService<ActivityManager>()!!.getRunningServices(Int.MAX_VALUE) return context.getSystemService<ActivityManager>()!!.getRunningServices(Int.MAX_VALUE)
.any { BackgroundMode::class.java.name == it.service.className } .any { OnlinePlayerService::class.java.name == it.service.className }
} }
} }

View File

@ -93,8 +93,7 @@ object ImageHelper {
* Get a squared bitmap with the same width and height from a bitmap * Get a squared bitmap with the same width and height from a bitmap
* @param bitmap The bitmap to resize * @param bitmap The bitmap to resize
*/ */
fun getSquareBitmap(bitmap: Bitmap?): Bitmap? { fun getSquareBitmap(bitmap: Bitmap): Bitmap {
bitmap ?: return null
val newSize = minOf(bitmap.width, bitmap.height) val newSize = minOf(bitmap.width, bitmap.height)
return Bitmap.createBitmap( return Bitmap.createBitmap(
bitmap, bitmap,

View File

@ -0,0 +1,10 @@
package com.github.libretube.obj
import java.nio.file.Path
data class PlayerNotificationData(
val title: String? = null,
val uploaderName: String? = null,
val thumbnailUrl: String? = null,
val thumbnailPath: Path? = null
)

View File

@ -0,0 +1,114 @@
package com.github.libretube.services
import android.app.Service
import android.content.Intent
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import com.github.libretube.R
import com.github.libretube.constants.BACKGROUND_CHANNEL_ID
import com.github.libretube.constants.IntentData
import com.github.libretube.constants.PLAYER_NOTIFICATION_ID
import com.github.libretube.db.DatabaseHolder
import com.github.libretube.db.obj.DownloadWithItems
import com.github.libretube.enums.FileType
import com.github.libretube.extensions.toAndroidUri
import com.github.libretube.helpers.PlayerHelper
import com.github.libretube.helpers.PlayerHelper.loadPlaybackParams
import com.github.libretube.obj.PlayerNotificationData
import com.github.libretube.util.NowPlayingNotification
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/**
* A service to play downloaded audio in the background
*/
class OfflinePlayerService : Service() {
private var player: ExoPlayer? = null
private var nowPlayingNotification: NowPlayingNotification? = null
override fun onCreate() {
super.onCreate()
val notification = NotificationCompat.Builder(this, BACKGROUND_CHANNEL_ID)
.setContentTitle(getString(R.string.app_name))
.setContentText(getString(R.string.playingOnBackground))
.setSmallIcon(R.drawable.ic_launcher_lockscreen)
.build()
startForeground(PLAYER_NOTIFICATION_ID, notification)
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
val videoId = intent.getStringExtra(IntentData.videoId)
CoroutineScope(Dispatchers.IO).launch {
val downloadWithItems = DatabaseHolder.Database.downloadDao().findById(videoId!!)
withContext(Dispatchers.Main) {
if (startAudioPlayer(downloadWithItems)) {
nowPlayingNotification = NowPlayingNotification(
this@OfflinePlayerService,
player!!,
true
)
val notificationData = PlayerNotificationData(
title = downloadWithItems.download.title,
uploaderName = downloadWithItems.download.uploader,
thumbnailPath = downloadWithItems.download.thumbnailPath
)
nowPlayingNotification?.updatePlayerNotification(videoId, notificationData)
} else {
onDestroy()
}
}
}
return super.onStartCommand(intent, flags, startId)
}
/**
* Attempt to start an audio player with the given download items
* @param downloadWithItem The database download to play from
* @return whether starting the audio player succeeded
*/
private fun startAudioPlayer(downloadWithItem: DownloadWithItems): Boolean {
player = ExoPlayer.Builder(this)
.setUsePlatformDiagnostics(false)
.setHandleAudioBecomingNoisy(true)
.setAudioAttributes(PlayerHelper.getAudioAttributes(), true)
.setLoadControl(PlayerHelper.getLoadControl())
.build()
.loadPlaybackParams(isBackgroundMode = true).apply {
playWhenReady = true
}
val audioItem = downloadWithItem.downloadItems.firstOrNull { it.type == FileType.AUDIO }
?: // in some rare cases, video files can contain audio
downloadWithItem.downloadItems.firstOrNull { it.type == FileType.VIDEO } ?: return false
val mediaItem = MediaItem.Builder()
.setUri(audioItem.path.toAndroidUri())
.build()
player?.setMediaItem(mediaItem)
player?.prepare()
return true
}
override fun onDestroy() {
nowPlayingNotification?.destroySelfAndPlayer()
player = null
nowPlayingNotification = null
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
stopSelf()
super.onDestroy()
}
override fun onBind(intent: Intent?) = null
}

View File

@ -3,12 +3,12 @@ package com.github.libretube.services
import android.app.Notification import android.app.Notification
import android.content.Intent import android.content.Intent
import android.os.Binder import android.os.Binder
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.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -28,6 +28,7 @@ import com.github.libretube.helpers.PlayerHelper
import com.github.libretube.helpers.PlayerHelper.checkForSegments import com.github.libretube.helpers.PlayerHelper.checkForSegments
import com.github.libretube.helpers.PlayerHelper.loadPlaybackParams import com.github.libretube.helpers.PlayerHelper.loadPlaybackParams
import com.github.libretube.helpers.ProxyHelper import com.github.libretube.helpers.ProxyHelper
import com.github.libretube.obj.PlayerNotificationData
import com.github.libretube.util.NowPlayingNotification import com.github.libretube.util.NowPlayingNotification
import com.github.libretube.util.PlayingQueue import com.github.libretube.util.PlayingQueue
import com.google.android.exoplayer2.ExoPlayer import com.google.android.exoplayer2.ExoPlayer
@ -44,7 +45,7 @@ import kotlinx.serialization.encodeToString
/** /**
* Loads the selected videos audio in background mode with a notification area. * Loads the selected videos audio in background mode with a notification area.
*/ */
class BackgroundMode : LifecycleService() { class OnlinePlayerService : LifecycleService() {
/** /**
* VideoId of the video * VideoId of the video
*/ */
@ -97,9 +98,8 @@ class BackgroundMode : LifecycleService() {
*/ */
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// see https://developer.android.com/reference/android/app/Service#startForeground(int,%20android.app.Notification) val notification = NotificationCompat.Builder(this, BACKGROUND_CHANNEL_ID)
val notification = Notification.Builder(this, BACKGROUND_CHANNEL_ID)
.setContentTitle(getString(R.string.app_name)) .setContentTitle(getString(R.string.app_name))
.setContentText(getString(R.string.playingOnBackground)) .setContentText(getString(R.string.playingOnBackground))
.setSmallIcon(R.drawable.ic_launcher_lockscreen) .setSmallIcon(R.drawable.ic_launcher_lockscreen)
@ -107,7 +107,6 @@ class BackgroundMode : LifecycleService() {
startForeground(PLAYER_NOTIFICATION_ID, notification) startForeground(PLAYER_NOTIFICATION_ID, notification)
} }
}
/** /**
* Initializes the [player] with the [MediaItem]. * Initializes the [player] with the [MediaItem].
@ -189,10 +188,19 @@ class BackgroundMode : LifecycleService() {
setMediaItem() setMediaItem()
// create the notification // create the notification
if (!this@BackgroundMode::nowPlayingNotification.isInitialized) { if (!this@OnlinePlayerService::nowPlayingNotification.isInitialized) {
nowPlayingNotification = NowPlayingNotification(this@BackgroundMode, player!!, true) nowPlayingNotification = NowPlayingNotification(
this@OnlinePlayerService,
player!!,
true
)
} }
nowPlayingNotification.updatePlayerNotification(videoId, streams!!) val playerNotificationData = PlayerNotificationData(
streams?.title,
streams?.uploader,
streams?.thumbnailUrl
)
nowPlayingNotification.updatePlayerNotification(videoId, playerNotificationData)
player?.apply { player?.apply {
playWhenReady = playWhenReadyPlayer playWhenReady = playWhenReadyPlayer
@ -259,7 +267,7 @@ class BackgroundMode : LifecycleService() {
// show a toast on errors // show a toast on errors
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
Toast.makeText( Toast.makeText(
this@BackgroundMode.applicationContext, this@OnlinePlayerService.applicationContext,
error.localizedMessage, error.localizedMessage,
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
@ -351,7 +359,7 @@ class BackgroundMode : LifecycleService() {
} }
/** /**
* destroy the [BackgroundMode] foreground service * destroy the [OnlinePlayerService] foreground service
*/ */
override fun onDestroy() { override fun onDestroy() {
// clear and reset the playing queue // clear and reset the playing queue
@ -371,7 +379,7 @@ class BackgroundMode : LifecycleService() {
inner class LocalBinder : Binder() { inner class LocalBinder : Binder() {
// Return this instance of [BackgroundMode] so clients can call public methods // Return this instance of [BackgroundMode] so clients can call public methods
fun getService(): BackgroundMode = this@BackgroundMode fun getService(): OnlinePlayerService = this@OnlinePlayerService
} }
override fun onBind(intent: Intent): IBinder { override fun onBind(intent: Intent): IBinder {

View File

@ -77,6 +77,7 @@ class OfflinePlayerActivity : BaseActivity() {
.setTrackSelector(trackSelector) .setTrackSelector(trackSelector)
.setLoadControl(PlayerHelper.getLoadControl()) .setLoadControl(PlayerHelper.getLoadControl())
.setAudioAttributes(PlayerHelper.getAudioAttributes(), true) .setAudioAttributes(PlayerHelper.getAudioAttributes(), true)
.setUsePlatformDiagnostics(false)
.build().apply { .build().apply {
addListener(object : Player.Listener { addListener(object : Player.Listener {
override fun onEvents(player: Player, events: Player.Events) { override fun onEvents(player: Player, events: Player.Events) {

View File

@ -6,27 +6,24 @@ import android.content.Intent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.constants.IntentData import com.github.libretube.constants.IntentData
import com.github.libretube.databinding.DownloadedMediaRowBinding import com.github.libretube.databinding.DownloadedMediaRowBinding
import com.github.libretube.db.DatabaseHolder
import com.github.libretube.db.obj.DownloadWithItems import com.github.libretube.db.obj.DownloadWithItems
import com.github.libretube.extensions.formatAsFileSize import com.github.libretube.extensions.formatAsFileSize
import com.github.libretube.helpers.ImageHelper import com.github.libretube.helpers.ImageHelper
import com.github.libretube.ui.activities.OfflinePlayerActivity import com.github.libretube.ui.activities.OfflinePlayerActivity
import com.github.libretube.ui.sheets.DownloadOptionsBottomSheet
import com.github.libretube.ui.viewholders.DownloadsViewHolder import com.github.libretube.ui.viewholders.DownloadsViewHolder
import com.github.libretube.util.TextUtils import com.github.libretube.util.TextUtils
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlin.io.path.deleteIfExists
import kotlin.io.path.fileSize import kotlin.io.path.fileSize
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
class DownloadsAdapter( class DownloadsAdapter(
private val context: Context, private val context: Context,
private val downloads: MutableList<DownloadWithItems>, private val downloads: MutableList<DownloadWithItems>,
private val toogleDownload: (DownloadWithItems) -> Boolean private val toggleDownload: (DownloadWithItems) -> Boolean
) : RecyclerView.Adapter<DownloadsViewHolder>() { ) : RecyclerView.Adapter<DownloadsViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadsViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadsViewHolder {
val binding = DownloadedMediaRowBinding.inflate( val binding = DownloadedMediaRowBinding.inflate(
@ -75,7 +72,7 @@ class DownloadsAdapter(
} }
progressBar.setOnClickListener { progressBar.setOnClickListener {
val isDownloading = toogleDownload(downloads[position]) val isDownloading = toggleDownload(downloads[position])
resumePauseBtn.setImageResource( resumePauseBtn.setImageResource(
if (isDownloading) { if (isDownloading) {
@ -93,24 +90,13 @@ class DownloadsAdapter(
} }
root.setOnLongClickListener { root.setOnLongClickListener {
MaterialAlertDialogBuilder(root.context) DownloadOptionsBottomSheet(download, items) {
.setTitle(R.string.delete)
.setMessage(R.string.irreversible)
.setPositiveButton(R.string.okay) { _, _ ->
items.forEach {
it.path.deleteIfExists()
}
download.thumbnailPath?.deleteIfExists()
runBlocking(Dispatchers.IO) {
DatabaseHolder.Database.downloadDao().deleteDownload(download)
}
downloads.removeAt(position) downloads.removeAt(position)
notifyItemRemoved(position) notifyItemRemoved(position)
notifyItemRangeChanged(position, itemCount) notifyItemRangeChanged(position, itemCount)
} }.show(
.setNegativeButton(R.string.cancel, null) (root.context as AppCompatActivity).supportFragmentManager
.show() )
true true
} }
} }

View File

@ -29,7 +29,7 @@ import com.github.libretube.helpers.BackgroundHelper
import com.github.libretube.helpers.ImageHelper import com.github.libretube.helpers.ImageHelper
import com.github.libretube.helpers.NavigationHelper import com.github.libretube.helpers.NavigationHelper
import com.github.libretube.obj.ShareData import com.github.libretube.obj.ShareData
import com.github.libretube.services.BackgroundMode import com.github.libretube.services.OnlinePlayerService
import com.github.libretube.ui.activities.MainActivity import com.github.libretube.ui.activities.MainActivity
import com.github.libretube.ui.dialogs.ShareDialog import com.github.libretube.ui.dialogs.ShareDialog
import com.github.libretube.ui.interfaces.AudioPlayerOptions import com.github.libretube.ui.interfaces.AudioPlayerOptions
@ -59,13 +59,13 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
private var handler = Handler(Looper.getMainLooper()) private var handler = Handler(Looper.getMainLooper())
private var isPaused: Boolean = false private var isPaused: Boolean = false
private var playerService: BackgroundMode? = null private var playerService: OnlinePlayerService? = null
/** Defines callbacks for service binding, passed to bindService() */ /** Defines callbacks for service binding, passed to bindService() */
private val connection = object : ServiceConnection { private val connection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) { override fun onServiceConnected(className: ComponentName, service: IBinder) {
// We've bound to LocalService, cast the IBinder and get LocalService instance // We've bound to LocalService, cast the IBinder and get LocalService instance
val binder = service as BackgroundMode.LocalBinder val binder = service as OnlinePlayerService.LocalBinder
playerService = binder.getService() playerService = binder.getService()
handleServiceConnection() handleServiceConnection()
} }
@ -77,7 +77,7 @@ class AudioPlayerFragment : Fragment(), AudioPlayerOptions {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
audioHelper = AudioHelper(requireContext()) audioHelper = AudioHelper(requireContext())
Intent(activity, BackgroundMode::class.java).also { intent -> Intent(activity, OnlinePlayerService::class.java).also { intent ->
activity?.bindService(intent, connection, Context.BIND_AUTO_CREATE) activity?.bindService(intent, connection, Context.BIND_AUTO_CREATE)
} }
} }

View File

@ -73,6 +73,7 @@ import com.github.libretube.helpers.PlayerHelper.checkForSegments
import com.github.libretube.helpers.PlayerHelper.loadPlaybackParams import com.github.libretube.helpers.PlayerHelper.loadPlaybackParams
import com.github.libretube.helpers.PreferenceHelper import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.helpers.ProxyHelper import com.github.libretube.helpers.ProxyHelper
import com.github.libretube.obj.PlayerNotificationData
import com.github.libretube.obj.ShareData import com.github.libretube.obj.ShareData
import com.github.libretube.obj.VideoResolution import com.github.libretube.obj.VideoResolution
import com.github.libretube.services.DownloadService import com.github.libretube.services.DownloadService
@ -1365,7 +1366,12 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
if (!this::nowPlayingNotification.isInitialized) { if (!this::nowPlayingNotification.isInitialized) {
nowPlayingNotification = NowPlayingNotification(requireContext(), exoPlayer, false) nowPlayingNotification = NowPlayingNotification(requireContext(), exoPlayer, false)
} }
nowPlayingNotification.updatePlayerNotification(videoId!!, streams) val playerNotificationData = PlayerNotificationData(
streams.title,
streams.uploader,
streams.thumbnailUrl
)
nowPlayingNotification.updatePlayerNotification(videoId!!, playerNotificationData)
} }
/** /**

View File

@ -0,0 +1,55 @@
package com.github.libretube.ui.sheets
import android.content.Intent
import android.os.Bundle
import androidx.core.content.ContextCompat
import com.github.libretube.R
import com.github.libretube.constants.IntentData
import com.github.libretube.db.DatabaseHolder
import com.github.libretube.db.obj.Download
import com.github.libretube.db.obj.DownloadItem
import com.github.libretube.services.OfflinePlayerService
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlin.io.path.deleteIfExists
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
class DownloadOptionsBottomSheet(
private val download: Download,
private val items: List<DownloadItem>,
private val onDelete: () -> Unit
) : BaseBottomSheet() {
override fun onCreate(savedInstanceState: Bundle?) {
val options = listOf(R.string.playOnBackground, R.string.delete).map { getString(it) }
setSimpleItems(options) { selectedIndex ->
when (selectedIndex) {
0 -> {
val playerIntent = Intent(requireContext(), OfflinePlayerService::class.java)
.putExtra(IntentData.videoId, download.videoId)
ContextCompat.startForegroundService(requireContext(), playerIntent)
}
1 -> {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.delete)
.setMessage(R.string.irreversible)
.setPositiveButton(R.string.okay) { _, _ ->
items.forEach {
it.path.deleteIfExists()
}
download.thumbnailPath?.deleteIfExists()
runBlocking(Dispatchers.IO) {
DatabaseHolder.Database.downloadDao().deleteDownload(download)
}
onDelete.invoke()
dialog?.dismiss()
}
.setNegativeButton(R.string.cancel, null)
.show()
}
}
}
super.onCreate(savedInstanceState)
}
}

View File

@ -21,12 +21,12 @@ import androidx.core.graphics.drawable.toBitmap
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import coil.request.ImageRequest import coil.request.ImageRequest
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.api.obj.Streams
import com.github.libretube.constants.BACKGROUND_CHANNEL_ID import com.github.libretube.constants.BACKGROUND_CHANNEL_ID
import com.github.libretube.constants.IntentData import com.github.libretube.constants.IntentData
import com.github.libretube.constants.PLAYER_NOTIFICATION_ID import com.github.libretube.constants.PLAYER_NOTIFICATION_ID
import com.github.libretube.helpers.ImageHelper import com.github.libretube.helpers.ImageHelper
import com.github.libretube.helpers.PlayerHelper import com.github.libretube.helpers.PlayerHelper
import com.github.libretube.obj.PlayerNotificationData
import com.github.libretube.ui.activities.MainActivity import com.github.libretube.ui.activities.MainActivity
import com.google.android.exoplayer2.ExoPlayer import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.Player
@ -41,11 +41,11 @@ class NowPlayingNotification(
private val isBackgroundPlayerNotification: Boolean private val isBackgroundPlayerNotification: Boolean
) { ) {
private var videoId: String? = null private var videoId: String? = null
private var streams: Streams? = null private var notificationData: PlayerNotificationData? = null
private var bitmap: Bitmap? = null private var bitmap: Bitmap? = null
/** /**
* The [MediaSessionCompat] for the [streams]. * The [MediaSessionCompat] for the [notificationData].
*/ */
private lateinit var mediaSession: MediaSessionCompat private lateinit var mediaSession: MediaSessionCompat
@ -68,7 +68,7 @@ class NowPlayingNotification(
* sets the title of the notification * sets the title of the notification
*/ */
override fun getCurrentContentTitle(player: Player): CharSequence { override fun getCurrentContentTitle(player: Player): CharSequence {
return streams?.title!! return notificationData?.title.orEmpty()
} }
/** /**
@ -92,7 +92,7 @@ class NowPlayingNotification(
* the description of the notification (below the title) * the description of the notification (below the title)
*/ */
override fun getCurrentContentText(player: Player): CharSequence? { override fun getCurrentContentText(player: Player): CharSequence? {
return streams?.uploader return notificationData?.uploaderName
} }
/** /**
@ -110,21 +110,25 @@ class NowPlayingNotification(
} }
override fun getCurrentSubText(player: Player): CharSequence? { override fun getCurrentSubText(player: Player): CharSequence? {
return streams?.uploader return notificationData?.uploaderName
} }
} }
private fun enqueueThumbnailRequest(callback: PlayerNotificationManager.BitmapCallback) { private fun enqueueThumbnailRequest(callback: PlayerNotificationManager.BitmapCallback) {
val request = ImageRequest.Builder(context) // If playing a downloaded file, show the downloaded thumbnail instead of loading an
.data(streams?.thumbnailUrl) // online image
.target { notificationData?.thumbnailPath?.let { path ->
val bm = it.toBitmap() ImageHelper.getDownloadedImage(context, path)?.let {
// returns the bitmap on Android 13+, for everything below scaled down to a square bitmap = processThumbnailBitmap(it)
bitmap = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { callback.onBitmap(bitmap!!)
ImageHelper.getSquareBitmap(bm)
} else {
bm
} }
return
}
val request = ImageRequest.Builder(context)
.data(notificationData?.thumbnailUrl)
.target {
bitmap = processThumbnailBitmap(it.toBitmap())
callback.onBitmap(bitmap!!) callback.onBitmap(bitmap!!)
} }
.build() .build()
@ -155,6 +159,17 @@ class NowPlayingNotification(
} }
} }
/**
* Returns the bitmap on Android 13+, for everything below scaled down to a square
*/
private fun processThumbnailBitmap(bitmap: Bitmap): Bitmap {
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
ImageHelper.getSquareBitmap(bitmap)
} else {
bitmap
}
}
private fun createNotificationAction(drawableRes: Int, actionName: String, instanceId: Int): NotificationCompat.Action { private fun createNotificationAction(drawableRes: Int, actionName: String, instanceId: Int): NotificationCompat.Action {
val intent = Intent(actionName).setPackage(context.packageName) val intent = Intent(actionName).setPackage(context.packageName)
val pendingIntent = PendingIntentCompat val pendingIntent = PendingIntentCompat
@ -194,16 +209,14 @@ class NowPlayingNotification(
context.resources, context.resources,
R.drawable.ic_launcher_monochrome R.drawable.ic_launcher_monochrome
) )
val title = streams?.title!!
val uploader = streams?.uploader
val extras = bundleOf( val extras = bundleOf(
MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON to appIcon, MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON to appIcon,
MediaMetadataCompat.METADATA_KEY_TITLE to title, MediaMetadataCompat.METADATA_KEY_TITLE to notificationData?.title,
MediaMetadataCompat.METADATA_KEY_ARTIST to uploader MediaMetadataCompat.METADATA_KEY_ARTIST to notificationData?.uploaderName
) )
return MediaDescriptionCompat.Builder() return MediaDescriptionCompat.Builder()
.setTitle(title) .setTitle(notificationData?.title)
.setSubtitle(uploader) .setSubtitle(notificationData?.uploaderName)
.setIconBitmap(appIcon) .setIconBitmap(appIcon)
.setExtras(extras) .setExtras(extras)
.build() .build()
@ -252,10 +265,10 @@ class NowPlayingNotification(
*/ */
fun updatePlayerNotification( fun updatePlayerNotification(
videoId: String, videoId: String,
streams: Streams data: PlayerNotificationData
) { ) {
this.videoId = videoId this.videoId = videoId
this.streams = streams this.notificationData = data
// reset the thumbnail bitmap in order to become reloaded for the new video // reset the thumbnail bitmap in order to become reloaded for the new video
this.bitmap = null this.bitmap = null