mirror of
https://github.com/jellyfin/jellyfin-android.git
synced 2026-10-23 00:02:33 +02:00
Refactor image loading, add icon to background playback notification
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package org.jellyfin.mobile
|
||||
|
||||
import coil.ImageLoader
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import okhttp3.OkHttpClient
|
||||
import org.jellyfin.apiclient.AppInfo
|
||||
@@ -21,6 +22,7 @@ const val WEBAPP_FUNCTION_CHANNEL = "WebAppFunctionChannel"
|
||||
val applicationModule: Module = module {
|
||||
single { AppPreferences(androidApplication()) }
|
||||
single { OkHttpClient() }
|
||||
single { ImageLoader(androidApplication()) }
|
||||
single(named(PLAYER_EVENT_CHANNEL)) { Channel<PlayerEvent>() }
|
||||
single(named(WEBAPP_FUNCTION_CHANNEL)) { Channel<String>() }
|
||||
single {
|
||||
|
||||
@@ -8,9 +8,20 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import coil.ImageLoader
|
||||
import coil.request.GetRequestBuilder
|
||||
import com.google.android.exoplayer2.Player
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.apiclient.model.dto.ImageOptions
|
||||
import org.jellyfin.apiclient.model.entities.ImageType.Primary
|
||||
import org.jellyfin.mobile.AppPreferences
|
||||
import org.jellyfin.mobile.BuildConfig
|
||||
import org.jellyfin.mobile.R
|
||||
@@ -22,9 +33,10 @@ import org.koin.core.inject
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
class PlayerNotificationHelper(private val viewModel: PlayerViewModel) : KoinComponent {
|
||||
private val appPreferences: AppPreferences by inject()
|
||||
private val context: Context = viewModel.getApplication<Application>()
|
||||
private val appPreferences: AppPreferences by inject()
|
||||
private val notificationManager: NotificationManager? by lazy { context.getSystemService() }
|
||||
private val imageLoader: ImageLoader by inject()
|
||||
private val receiverRegistered = AtomicBoolean(false)
|
||||
|
||||
val shouldShowNotification: Boolean
|
||||
@@ -36,41 +48,55 @@ class PlayerNotificationHelper(private val viewModel: PlayerViewModel) : KoinCom
|
||||
val nm = notificationManager ?: return
|
||||
val mediaSource = viewModel.mediaSourceManager.jellyfinMediaSource.value ?: return
|
||||
val player = viewModel.playerOrNull ?: return
|
||||
val playbackState = player.playbackState
|
||||
if (playbackState != Player.STATE_READY && playbackState != Player.STATE_BUFFERING) return
|
||||
|
||||
// Create notification channel
|
||||
context.createMediaNotificationChannel(nm)
|
||||
|
||||
val style = Notification.MediaStyle().apply {
|
||||
setMediaSession(viewModel.mediaSession.sessionToken)
|
||||
// FIXME Doesn't work properly right now, requires large icon first (primary image via apiclient)
|
||||
//setShowActionsInCompactView(0, 1, 2)
|
||||
viewModel.viewModelScope.launch {
|
||||
val mediaIcon: Bitmap? = withContext(Dispatchers.IO) {
|
||||
val imageUrl = viewModel.apiClient.GetImageUrl(mediaSource.id, ImageOptions().apply {
|
||||
imageType = Primary
|
||||
val size = context.resources.getDimensionPixelSize(R.dimen.media_notification_height)
|
||||
maxWidth = size
|
||||
maxHeight = size
|
||||
})
|
||||
imageLoader.execute(GetRequestBuilder(context).data(imageUrl).build()).drawable?.toBitmap()
|
||||
}
|
||||
|
||||
val style = Notification.MediaStyle().apply {
|
||||
setMediaSession(viewModel.mediaSession.sessionToken)
|
||||
setShowActionsInCompactView(0, 1, 2)
|
||||
}
|
||||
|
||||
val notification = Notification.Builder(context).apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
setChannelId(Constants.MEDIA_NOTIFICATION_CHANNEL_ID) // Set Notification Channel on Android O and above
|
||||
setColorized(true)
|
||||
} else {
|
||||
setPriority(Notification.PRIORITY_LOW)
|
||||
}
|
||||
setSmallIcon(R.drawable.ic_notification)
|
||||
mediaIcon?.let(::setLargeIcon)
|
||||
setContentTitle(mediaSource.title)
|
||||
mediaSource.artists?.let(::setContentText)
|
||||
setStyle(style)
|
||||
setVisibility(Notification.VISIBILITY_PUBLIC)
|
||||
addAction(generateAction(R.drawable.ic_rewind_black_32dp, R.string.notification_action_rewind, Constants.ACTION_REWIND))
|
||||
val playbackAction = when {
|
||||
!player.playWhenReady -> generateAction(R.drawable.ic_play_black_42dp, R.string.notification_action_play, Constants.ACTION_PLAY)
|
||||
else -> generateAction(R.drawable.ic_pause_black_42dp, R.string.notification_action_pause, Constants.ACTION_PAUSE)
|
||||
}
|
||||
addAction(playbackAction)
|
||||
addAction(generateAction(R.drawable.ic_fast_forward_black_32dp, R.string.notification_action_fast_forward, Constants.ACTION_FAST_FORWARD))
|
||||
setContentIntent(buildContentIntent())
|
||||
setDeleteIntent(buildDeleteIntent())
|
||||
}.build()
|
||||
|
||||
nm.notify(VIDEO_PLAYER_NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
val notification = Notification.Builder(context).apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
setChannelId(Constants.MEDIA_NOTIFICATION_CHANNEL_ID) // Set Notification Channel on Android O and above
|
||||
setColorized(true)
|
||||
} else {
|
||||
setPriority(Notification.PRIORITY_LOW)
|
||||
}
|
||||
setSmallIcon(R.drawable.ic_notification)
|
||||
setContentTitle(mediaSource.title)
|
||||
mediaSource.artists?.let(::setContentText)
|
||||
setStyle(style)
|
||||
setVisibility(Notification.VISIBILITY_PUBLIC)
|
||||
addAction(generateAction(R.drawable.ic_rewind_black_32dp, R.string.notification_action_rewind, Constants.ACTION_REWIND))
|
||||
val playbackAction = when {
|
||||
!player.playWhenReady -> generateAction(R.drawable.ic_play_black_42dp, R.string.notification_action_play, Constants.ACTION_PLAY)
|
||||
else -> generateAction(R.drawable.ic_pause_black_42dp, R.string.notification_action_pause, Constants.ACTION_PAUSE)
|
||||
}
|
||||
addAction(playbackAction)
|
||||
addAction(generateAction(R.drawable.ic_fast_forward_black_32dp, R.string.notification_action_fast_forward, Constants.ACTION_FAST_FORWARD))
|
||||
setContentIntent(buildContentIntent())
|
||||
setDeleteIntent(buildDeleteIntent())
|
||||
}.build()
|
||||
|
||||
nm.notify(VIDEO_PLAYER_NOTIFICATION_ID, notification)
|
||||
|
||||
if (receiverRegistered.compareAndSet(false, true)) {
|
||||
context.registerReceiver(notificationActionReceiver, IntentFilter().apply {
|
||||
addAction(Constants.ACTION_PLAY)
|
||||
|
||||
@@ -15,6 +15,7 @@ import com.google.android.exoplayer2.util.Clock
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.apiclient.interaction.ApiClient
|
||||
import org.jellyfin.mobile.BuildConfig
|
||||
import org.jellyfin.mobile.PLAYER_EVENT_CHANNEL
|
||||
import org.jellyfin.mobile.WEBAPP_FUNCTION_CHANNEL
|
||||
@@ -27,6 +28,7 @@ import org.koin.core.inject
|
||||
import org.koin.core.qualifier.named
|
||||
|
||||
class PlayerViewModel(application: Application) : AndroidViewModel(application), KoinComponent, Player.EventListener {
|
||||
val apiClient: ApiClient by inject()
|
||||
val mediaSourceManager = MediaSourceManager(this)
|
||||
val notificationHelper: PlayerNotificationHelper by lazy { PlayerNotificationHelper(this) }
|
||||
private val lifecycleObserver = PlayerLifecycleObserver(this)
|
||||
|
||||
@@ -23,7 +23,7 @@ import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import coil.Coil
|
||||
import coil.ImageLoader
|
||||
import coil.request.GetRequest
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -71,12 +71,12 @@ class RemotePlayerService : Service(), CoroutineScope {
|
||||
get() = job + Dispatchers.Main
|
||||
|
||||
private val appPreferences: AppPreferences by inject()
|
||||
private val notificationManager: NotificationManager by lazy { getSystemService()!! }
|
||||
private val imageLoader: ImageLoader by inject()
|
||||
|
||||
private val binder = ServiceBinder(this)
|
||||
|
||||
private lateinit var wakeLock: PowerManager.WakeLock
|
||||
|
||||
private val notificationManager: NotificationManager by lazy { getSystemService()!! }
|
||||
private var mediaSession: MediaSession? = null
|
||||
private var mediaController: MediaController? = null
|
||||
private var largeItemIcon: Bitmap? = null
|
||||
@@ -208,7 +208,7 @@ class RemotePlayerService : Service(), CoroutineScope {
|
||||
if (imageUrl != null && imageUrl.isNotEmpty()) {
|
||||
launch {
|
||||
val request = GetRequest.Builder(this@RemotePlayerService).data(imageUrl).build()
|
||||
val bitmap = Coil.imageLoader(this@RemotePlayerService).execute(request).drawable?.toBitmap()
|
||||
val bitmap = imageLoader.execute(request).drawable?.toBitmap()
|
||||
largeItemIcon = bitmap
|
||||
notifyWithBitmap(handledIntent, bitmap)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<dimen name="media_notification_height">140dp</dimen>
|
||||
</resources>
|
||||
@@ -1,5 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<dimen name="media_notification_height">128dp</dimen>
|
||||
|
||||
<dimen name="exo_title_text_size">16sp</dimen>
|
||||
<dimen name="exo_player_controls_display_padding">12dp</dimen>
|
||||
<dimen name="exo_center_icon_padding">12dp</dimen>
|
||||
@@ -7,4 +9,4 @@
|
||||
<dimen name="exo_bottom_controls_size">42dp</dimen>
|
||||
<dimen name="exo_bottom_controls_padding">8dp</dimen>
|
||||
<dimen name="exo_bottom_controls_margin">8dp</dimen>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -83,7 +83,7 @@ object Dependencies {
|
||||
|
||||
object Network {
|
||||
const val okHttp = "com.squareup.okhttp3:okhttp:${Versions.okHttp}"
|
||||
const val coil = "io.coil-kt:coil:${Versions.coil}"
|
||||
const val coil = "io.coil-kt:coil-base:${Versions.coil}"
|
||||
val exoPlayerHLS = exoPlayer("hls")
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user