mirror of
https://github.com/libre-tube/LibreTube.git
synced 2024-12-15 06:40:30 +05:30
Audio/background player for downloads
This commit is contained in:
parent
57b7dfdda9
commit
dcdd1af176
@ -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" />
|
||||||
|
|
||||||
|
@ -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 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
)
|
@ -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
|
||||||
|
}
|
@ -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 {
|
@ -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) {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user