Browse Source

Migrate sleep timer from AlarmManager to a more reliable in-app implementation

Fixes #2360
As AlarmManager is not reliable on all API versions/OEM implementations
we want to get rid of itfor the sleep timer management and instead use
a custom implementation based on coroutine job.
merge-requests/1287/head
Nicolas Pomepuy 5 years ago
parent
commit
ea94a7ee34
  1. 1
      application/resources/src/main/java/org/videolan/resources/Constants.kt
  2. 6
      application/television/src/main/java/org/videolan/television/ui/audioplayer/AudioPlayerActivity.kt
  3. 51
      application/vlc-android/src/org/videolan/vlc/PlaybackService.kt
  4. 8
      application/vlc-android/src/org/videolan/vlc/gui/audio/AudioPlayer.kt
  5. 12
      application/vlc-android/src/org/videolan/vlc/gui/dialogs/SleepTimerDialog.kt
  6. 28
      application/vlc-android/src/org/videolan/vlc/gui/helpers/PlayerOptionsDelegate.kt
  7. 6
      application/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerOverlayDelegate.kt
  8. 1
      application/vlc-android/src/org/videolan/vlc/media/PlaylistManager.kt

1
application/resources/src/main/java/org/videolan/resources/Constants.kt

@ -101,7 +101,6 @@ const val ACTION_CONTENT_INDEXING = "action_content_indexing"
@JvmField val PLAY_FROM_VIDEOGRID = "gui.video.PLAY_FROM_VIDEOGRID".buildPkgString()
@JvmField val PLAY_FROM_SERVICE = "gui.video.PLAY_FROM_SERVICE".buildPkgString()
@JvmField val EXIT_PLAYER = "gui.video.EXIT_PLAYER".buildPkgString()
@JvmField val SLEEP_INTENT = "SleepIntent".buildPkgString()
const val PLAY_EXTRA_ITEM_LOCATION = "item_location"
const val PLAY_EXTRA_SUBTITLES_LOCATION = "subtitles_location"
const val PLAY_EXTRA_ITEM_TITLE = "title"

6
application/television/src/main/java/org/videolan/television/ui/audioplayer/AudioPlayerActivity.kt

@ -97,7 +97,7 @@ class AudioPlayerActivity : BaseTvActivity(),KeycodeListener {
updateRepeatMode()
}
model.speed.observe(this) { showChips() }
PlayerOptionsDelegate.playerSleepTime.observe(this) {
PlaybackService.playerSleepTime.observe(this) {
showChips()
}
binding.mediaProgress.setOnSeekBarChangeListener(timelineListener)
@ -123,7 +123,7 @@ class AudioPlayerActivity : BaseTvActivity(),KeycodeListener {
newFragment.show(supportFragmentManager, "time")
}
binding.sleepQuickAction.setOnLongClickListener {
model.service?.setSleep(null)
model.service?.setSleepTimer(null)
showChips()
true
}
@ -150,7 +150,7 @@ class AudioPlayerActivity : BaseTvActivity(),KeycodeListener {
if (it != 1.0F) binding.playbackSpeedQuickAction.setVisible()
binding.playbackSpeedQuickActionText.text = it.formatRateString()
}
PlayerOptionsDelegate.playerSleepTime.value?.let {
PlaybackService.playerSleepTime.value?.let {
binding.sleepQuickAction.setVisible()
binding.sleepQuickActionText.text = DateFormat.getTimeFormat(this).format(it.time)
}

51
application/vlc-android/src/org/videolan/vlc/PlaybackService.kt

@ -46,10 +46,7 @@ import androidx.core.app.ServiceCompat
import androidx.core.content.edit
import androidx.core.content.getSystemService
import androidx.core.os.bundleOf
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ServiceLifecycleDispatcher
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.*
import androidx.media.MediaBrowserServiceCompat
import androidx.media.session.MediaButtonReceiver
import kotlinx.coroutines.*
@ -89,7 +86,9 @@ private const val TAG = "VLC/PlaybackService"
@ExperimentalCoroutinesApi
@ObsoleteCoroutinesApi
class PlaybackService : MediaBrowserServiceCompat(), LifecycleOwner {
class PlaybackService : MediaBrowserServiceCompat(), LifecycleOwner, CoroutineScope {
override val coroutineContext = Dispatchers.IO + SupervisorJob()
private var position: Long = -1L
private val dispatcher = ServiceLifecycleDispatcher(this)
@ -111,6 +110,7 @@ class PlaybackService : MediaBrowserServiceCompat(), LifecycleOwner {
private lateinit var wakeLock: PowerManager.WakeLock
private val audioFocusHelper by lazy { VLCAudioFocusHelper(this) }
private lateinit var browserCallback: MediaBrowserCallback
var sleepTimerJob: Job? = null
// Playback management
internal lateinit var mediaSession: MediaSessionCompat
@ -159,11 +159,6 @@ class PlaybackService : MediaBrowserServiceCompat(), LifecycleOwner {
}
VLCAppWidgetProvider.ACTION_WIDGET_INIT -> updateWidget()
VLCAppWidgetProvider.ACTION_WIDGET_ENABLED, VLCAppWidgetProvider.ACTION_WIDGET_DISABLED -> updateHasWidget()
SLEEP_INTENT -> {
if (isPlaying) {
stop()
}
}
VLCAppWidgetProvider.ACTION_WIDGET_ENABLED, VLCAppWidgetProvider.ACTION_WIDGET_DISABLED -> updateHasWidget()
ACTION_CAR_MODE_EXIT -> MediaSessionBrowser.unbindExtensionConnection()
AudioManager.ACTION_AUDIO_BECOMING_NOISY -> if (detectHeadset) {
@ -624,7 +619,6 @@ class PlaybackService : MediaBrowserServiceCompat(), LifecycleOwner {
addAction(Intent.ACTION_HEADSET_PLUG)
addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
addAction(ACTION_CAR_MODE_EXIT)
addAction(SLEEP_INTENT)
addAction(CUSTOM_ACTION)
}
registerReceiver(receiver, filter)
@ -1707,6 +1701,39 @@ class PlaybackService : MediaBrowserServiceCompat(), LifecycleOwner {
}
}
/**
* Start the loop that checks for the sleep timer consumption
*/
private fun startSleepTimerJob() {
stopSleepTimerJob()
sleepTimerJob = launch {
while (isActive) {
playerSleepTime.value?.let {
if (System.currentTimeMillis() > it.timeInMillis) {
withContext(Dispatchers.Main) { if (isPlaying) stop() else setSleepTimer(null) }
}
}
delay(1000)
}
}
}
private fun stopSleepTimerJob() {
if (BuildConfig.DEBUG) Log.d("SleepTimer", "stopSleepTimerJob")
sleepTimerJob?.cancel()
sleepTimerJob = null
}
/**
* Change the sleep timer time
* @param time a [Calendar] object for the new sleep timer time. Set to null to cancel the sleep timer
*/
fun setSleepTimer(time: Calendar?) {
if (time != null && time.timeInMillis < System.currentTimeMillis()) return
playerSleepTime.value = time
if (time == null) stopSleepTimerJob() else startSleepTimerJob()
}
companion object {
val serviceFlow = MutableStateFlow<PlaybackService?>(null)
val instance : PlaybackService?
@ -1720,6 +1747,8 @@ class PlaybackService : MediaBrowserServiceCompat(), LifecycleOwner {
private const val SHOW_TOAST = 1
private const val END_MEDIASESSION = 2
val playerSleepTime by lazy(LazyThreadSafetyMode.NONE) { MutableLiveData<Calendar?>().apply { value = null } }
fun start(context: Context) {
if (instance != null) return
val serviceIntent = Intent(context, PlaybackService::class.java)

8
application/vlc-android/src/org/videolan/vlc/gui/audio/AudioPlayer.kt

@ -135,7 +135,7 @@ class AudioPlayer : Fragment(), PlaylistAdapter.IPlayer, TextWatcher, IAudioPlay
delay(50L)
}.launchWhenStarted(lifecycleScope)
bookmarkModel = BookmarkModel.get(requireActivity())
PlayerOptionsDelegate.playerSleepTime.observe(this@AudioPlayer) {
PlaybackService.playerSleepTime.observe(this@AudioPlayer) {
showChips()
}
Settings.setAudioControlsChangeListener {
@ -223,7 +223,7 @@ class AudioPlayer : Fragment(), PlaylistAdapter.IPlayer, TextWatcher, IAudioPlay
newFragment.show(requireActivity().supportFragmentManager, "time")
}
binding.sleepQuickAction.setOnLongClickListener {
playlistModel.service?.setSleep(null)
playlistModel.service?.setSleepTimer(null)
showChips()
true
}
@ -235,7 +235,7 @@ class AudioPlayer : Fragment(), PlaylistAdapter.IPlayer, TextWatcher, IAudioPlay
fun isTablet() = requireActivity().isTablet()
fun showChips() {
if (playlistModel.speed.value == 1.0F && PlayerOptionsDelegate.playerSleepTime.value == null) {
if (playlistModel.speed.value == 1.0F && PlaybackService.playerSleepTime.value == null) {
binding.playbackChips.setGone()
} else {
binding.playbackChips.setVisible()
@ -245,7 +245,7 @@ class AudioPlayer : Fragment(), PlaylistAdapter.IPlayer, TextWatcher, IAudioPlay
if (it != 1.0F) binding.playbackSpeedQuickAction.setVisible()
binding.playbackSpeedQuickAction.text = it.formatRateString()
}
PlayerOptionsDelegate.playerSleepTime.value?.let {
PlaybackService.playerSleepTime.value?.let {
binding.sleepQuickAction.setVisible()
binding.sleepQuickAction.text = DateFormat.getTimeFormat(requireContext()).format(it.time)
}

12
application/vlc-android/src/org/videolan/vlc/gui/dialogs/SleepTimerDialog.kt

@ -28,15 +28,17 @@ import android.view.View
import android.view.ViewGroup
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.ObsoleteCoroutinesApi
import org.videolan.vlc.PlaybackService
import org.videolan.vlc.R
import org.videolan.vlc.gui.helpers.PlayerOptionsDelegate
import org.videolan.vlc.gui.helpers.setSleep
import org.videolan.vlc.viewmodels.PlaylistModel
import java.util.*
@ObsoleteCoroutinesApi
@ExperimentalCoroutinesApi
class SleepTimerDialog : PickTimeFragment() {
private val playlistModel by lazy { PlaylistModel.get(this) }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val view = super.onCreateView(inflater, container, savedInstanceState)
@ -53,19 +55,19 @@ class SleepTimerDialog : PickTimeFragment() {
val sleepTime = Calendar.getInstance()
sleepTime.timeInMillis = sleepTime.timeInMillis + interval
sleepTime.set(Calendar.SECOND, 0)
requireContext().setSleep(sleepTime)
playlistModel.service?.setSleepTimer(sleepTime)
}
dismiss()
}
override fun showDeleteCurrent(): Boolean {
return PlayerOptionsDelegate.playerSleepTime != null
return PlaybackService.playerSleepTime != null
}
override fun onClick(v: View) {
if (v.id == R.id.tim_pic_delete_current) {
requireActivity().setSleep(null)
playlistModel.service?.setSleepTimer(null)
dismiss()
} else super.onClick(v)
}

28
application/vlc-android/src/org/videolan/vlc/gui/helpers/PlayerOptionsDelegate.kt

@ -1,11 +1,7 @@
package org.videolan.vlc.gui.helpers
import android.annotation.SuppressLint
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.support.v4.media.session.PlaybackStateCompat
import android.view.LayoutInflater
import android.view.View
@ -13,17 +9,18 @@ import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.Toast
import androidx.appcompat.widget.ViewStubCompat
import androidx.core.content.getSystemService
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentActivity
import androidx.leanback.widget.BrowseFrameLayout
import androidx.leanback.widget.BrowseFrameLayout.OnFocusSearchListener
import androidx.lifecycle.*
import androidx.lifecycle.LifecycleObserver
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.*
import org.videolan.resources.*
import org.videolan.tools.*
import org.videolan.resources.AndroidDevices
import org.videolan.resources.VLCOptions
import org.videolan.tools.AppScope
import org.videolan.tools.Settings
import org.videolan.vlc.PlaybackService
import org.videolan.vlc.R
import org.videolan.vlc.databinding.PlayerOptionItemBinding
@ -34,7 +31,6 @@ import org.videolan.vlc.gui.dialogs.*
import org.videolan.vlc.gui.helpers.UiTools.addToPlaylist
import org.videolan.vlc.gui.video.VideoPlayerActivity
import org.videolan.vlc.media.PlayerController
import java.util.*
private const val ACTION_AUDIO_DELAY = 2
private const val ACTION_SPU_DELAY = 3
@ -356,20 +352,6 @@ class PlayerOptionsDelegate(val activity: FragmentActivity, val service: Playbac
}
}
}
companion object {
val playerSleepTime by lazy(LazyThreadSafetyMode.NONE) { MutableLiveData<Calendar?>().apply { value = null } }
}
}
fun Context.setSleep(time: Calendar?) {
val alarmMgr = applicationContext.getSystemService<AlarmManager>()!!
val intent = Intent(SLEEP_INTENT)
val sleepPendingIntent = PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
if (time != null) alarmMgr.set(AlarmManager.RTC_WAKEUP, time.timeInMillis, sleepPendingIntent)
else alarmMgr.cancel(sleepPendingIntent)
PlayerOptionsDelegate.playerSleepTime.value = time
}
data class PlayerOption(val id: Long, val icon: Int, val title: String)

6
application/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerOverlayDelegate.kt

@ -484,7 +484,7 @@ class VideoPlayerOverlayDelegate (private val player: VideoPlayerActivity) {
true
}
hudRightBinding.sleepQuickAction.setOnLongClickListener {
player.setSleep(null)
player.service?.setSleepTimer(null)
showControls(true)
true
}
@ -695,14 +695,14 @@ class VideoPlayerOverlayDelegate (private val player: VideoPlayerActivity) {
hudRightBinding.videoSecondaryDisplay.contentDescription = player.resources.getString(if (secondary) R.string.video_remote_disable else R.string.video_remote_enable)
hudRightBinding.playlistToggle.visibility = if (show && player.service?.hasPlaylist() == true) View.VISIBLE else View.GONE
hudRightBinding.sleepQuickAction.visibility = if (show && PlayerOptionsDelegate.playerSleepTime.value != null) View.VISIBLE else View.GONE
hudRightBinding.sleepQuickAction.visibility = if (show && PlaybackService.playerSleepTime.value != null) View.VISIBLE else View.GONE
hudRightBinding.playbackSpeedQuickAction.visibility = if (show && player.service?.rate != 1.0F) View.VISIBLE else View.GONE
hudRightBinding.spuDelayQuickAction.visibility = if (show && player.service?.spuDelay != 0L) View.VISIBLE else View.GONE
hudRightBinding.audioDelayQuickAction.visibility = if (show && player.service?.audioDelay != 0L) View.VISIBLE else View.GONE
hudRightBinding.playbackSpeedQuickAction.text = player.service?.rate?.formatRateString()
val format = DateFormat.getTimeInstance(DateFormat.SHORT, Locale.getDefault())
PlayerOptionsDelegate.playerSleepTime.value?.let {
PlaybackService.playerSleepTime.value?.let {
hudRightBinding.sleepQuickAction.text = format.format(it.time)
}
hudRightBinding.spuDelayQuickAction.text = "${(player.service?.spuDelay ?: 0L) / 1000L} ms"

1
application/vlc-android/src/org/videolan/vlc/media/PlaylistManager.kt

@ -290,6 +290,7 @@ class PlaylistManager(val service: PlaybackService) : MediaWrapperList.EventList
}
}
}
service.setSleepTimer(null)
mediaList.removeEventListener(this)
previous.clear()
currentIndex = -1

Loading…
Cancel
Save