PlaybackService.kt 54 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*****************************************************************************
 * PlaybackService.kt
 * Copyright © 2011-2018 VLC authors and VideoLAN
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
 */

package org.videolan.vlc

import android.annotation.TargetApi
23
import android.app.*
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import android.appwidget.AppWidgetManager
import android.content.*
import android.media.AudioManager
import android.media.audiofx.AudioEffect
import android.net.Uri
import android.os.*
import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.MediaDescriptionCompat
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import android.telephony.TelephonyManager
import android.text.TextUtils
import android.util.Log
38
39
import android.view.View
import android.widget.TextView
40
import android.widget.Toast
41
import androidx.annotation.MainThread
42
import androidx.annotation.RequiresApi
43
44
45
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat
46
47
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
48
import androidx.lifecycle.Observer
49
50
import androidx.lifecycle.ServiceLifecycleDispatcher
import androidx.lifecycle.lifecycleScope
51
import androidx.media.MediaBrowserServiceCompat
52
import androidx.media.session.MediaButtonReceiver
Geoffrey Métais's avatar
Geoffrey Métais committed
53
import kotlinx.coroutines.*
54
55
56
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.channels.actor
57
import kotlinx.coroutines.flow.MutableStateFlow
58
59
import org.videolan.libvlc.MediaPlayer
import org.videolan.libvlc.RendererItem
60
61
import org.videolan.libvlc.interfaces.IMedia
import org.videolan.libvlc.interfaces.IVLCVout
62
import org.videolan.libvlc.util.AndroidUtil
63
import org.videolan.medialibrary.interfaces.Medialibrary
64
import org.videolan.medialibrary.interfaces.media.MediaWrapper
65
import org.videolan.resources.*
66
import org.videolan.tools.*
67
68
69
import org.videolan.vlc.gui.helpers.AudioUtil
import org.videolan.vlc.gui.helpers.BitmapUtil
import org.videolan.vlc.gui.helpers.NotificationHelper
70
import org.videolan.vlc.gui.helpers.getBitmapFromDrawable
71
72
import org.videolan.vlc.gui.video.PopupManager
import org.videolan.vlc.gui.video.VideoPlayerActivity
73
import org.videolan.vlc.media.MediaSessionBrowser
74
import org.videolan.vlc.media.MediaUtils
75
import org.videolan.vlc.media.PlayerController
76
77
78
79
80
import org.videolan.vlc.media.PlaylistManager
import org.videolan.vlc.util.*
import org.videolan.vlc.widget.VLCAppWidgetProvider
import org.videolan.vlc.widget.VLCAppWidgetProviderBlack
import org.videolan.vlc.widget.VLCAppWidgetProviderWhite
81
import videolan.org.commontools.LiveEvent
82
83
import java.util.*

84
85
private const val TAG = "VLC/PlaybackService"

86
@ExperimentalCoroutinesApi
Geoffrey Métais's avatar
Geoffrey Métais committed
87
@ObsoleteCoroutinesApi
88
class PlaybackService : MediaBrowserServiceCompat(), LifecycleOwner {
89
    private val dispatcher = ServiceLifecycleDispatcher(this)
90

91
92
    lateinit var playlistManager: PlaylistManager
        private set
93
94
    val mediaplayer: MediaPlayer
        get() = playlistManager.player.mediaplayer
95
    private lateinit var keyguardManager: KeyguardManager
96
    internal lateinit var settings: SharedPreferences
97
    private val binder = LocalBinder()
98
    internal lateinit var medialibrary: Medialibrary
99

100
    private val callbacks = mutableListOf<Callback>()
101
    private lateinit var cbActor : SendChannel<CbAction>
102
103
    private var detectHeadset = true
    private lateinit var wakeLock: PowerManager.WakeLock
104
    private val audioFocusHelper by lazy { VLCAudioFocusHelper(this) }
105
106

    // Playback management
107
    internal lateinit var mediaSession: MediaSessionCompat
108
109
    @Volatile
    private var notificationShowing = false
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130

    private var widget = 0
    /**
     * Last widget position update timestamp
     */
    private var widgetPositionTimestamp = System.currentTimeMillis()
    private var popupManager: PopupManager? = null

    private val receiver = object : BroadcastReceiver() {
        private var wasPlaying = false
        override fun onReceive(context: Context, intent: Intent) {
            val action = intent.action ?: return
            val state = intent.getIntExtra("state", 0)

            // skip all headsets events if there is a call
            val telManager = this@PlaybackService.applicationContext.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager
            if (telManager?.callState != TelephonyManager.CALL_STATE_IDLE) return

            /*
             * Launch the activity if needed
             */
Habib Kazemi's avatar
Habib Kazemi committed
131
            if (action.startsWith(ACTION_REMOTE_GENERIC) && !isPlaying && !playlistManager.hasCurrentMedia()) {
132
133
134
135
136
137
138
139
                packageManager.getLaunchIntentForPackage(packageName)?.let { context.startActivity(it) }
            }

            /*
             * Remote / headset control events
             */
            when (action) {
                VLCAppWidgetProvider.ACTION_WIDGET_INIT -> updateWidget()
140
                VLCAppWidgetProvider.ACTION_WIDGET_ENABLED, VLCAppWidgetProvider.ACTION_WIDGET_DISABLED -> updateHasWidget()
141
142
                SLEEP_INTENT -> {
                    if (isPlaying) {
Geoffrey Métais's avatar
Geoffrey Métais committed
143
                        stop()
144
145
                    }
                }
146
                VLCAppWidgetProvider.ACTION_WIDGET_ENABLED, VLCAppWidgetProvider.ACTION_WIDGET_DISABLED -> updateHasWidget()
147
                ACTION_CAR_MODE_EXIT -> MediaSessionBrowser.unbindExtensionConnection()
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
                AudioManager.ACTION_AUDIO_BECOMING_NOISY -> if (detectHeadset) {
                    if (BuildConfig.DEBUG) Log.i(TAG, "Becoming noisy")
                    wasPlaying = isPlaying
                    if (wasPlaying && playlistManager.hasCurrentMedia())
                        pause()
                }
                Intent.ACTION_HEADSET_PLUG -> if (detectHeadset && state != 0) {
                    if (BuildConfig.DEBUG) Log.i(TAG, "Headset Inserted.")
                    if (wasPlaying && playlistManager.hasCurrentMedia() && settings.getBoolean("enable_play_on_headset_insertion", false))
                        play()
                }
            }/*
             * headset plug events
             */
        }
    }

    private val mediaPlayerListener = MediaPlayer.EventListener { event ->
        when (event.type) {
            MediaPlayer.Event.Playing -> {
                if (BuildConfig.DEBUG) Log.i(TAG, "MediaPlayer.Event.Playing")
                executeUpdate()
                publishState()
171
                audioFocusHelper.changeAudioFocus(true)
172
                if (!wakeLock.isHeld) wakeLock.acquire()
173
                showNotification()
174
                handler.nbErrors = 0
175
176
177
178
179
180
181
182
183
            }
            MediaPlayer.Event.Paused -> {
                if (BuildConfig.DEBUG) Log.i(TAG, "MediaPlayer.Event.Paused")
                executeUpdate()
                publishState()
                showNotification()
                if (wakeLock.isHeld) wakeLock.release()
            }
            MediaPlayer.Event.EncounteredError -> executeUpdate()
184
            MediaPlayer.Event.PositionChanged -> if (widget != 0) updateWidgetPosition(event.positionChanged)
185
            MediaPlayer.Event.ESAdded -> if (event.esChangedType == IMedia.Track.Type.Video && (playlistManager.videoBackground || !playlistManager.switchToVideo())) {
186
187
188
                /* CbAction notification content intent: resume video or resume audio activity */
                updateMetadata()
            }
189
            MediaPlayer.Event.MediaChanged -> if (BuildConfig.DEBUG) Log.d(TAG, "onEvent: MediaChanged")
190
        }
191
        cbActor.safeOffer(CbMediaPlayerEvent(event))
192
193
194
195
196
197
198
199
200
201
202
    }

    private val handler = PlaybackServiceHandler(this)

    val sessionPendingIntent: PendingIntent
        get() {
            return when {
                playlistManager.player.isVideoPlaying() -> {//PIP
                    val notificationIntent = Intent(this, VideoPlayerActivity::class.java)
                    PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT)
                }
203
                playlistManager.videoBackground || canSwitchToVideo() && !currentMediaHasFlag(MediaWrapper.MEDIA_FORCE_AUDIO) -> {//resume video playback
204
                    /* Resume VideoPlayerActivity from ACTION_REMOTE_SWITCH_VIDEO intent */
Habib Kazemi's avatar
Habib Kazemi committed
205
                    val notificationIntent = Intent(ACTION_REMOTE_SWITCH_VIDEO)
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
                    PendingIntent.getBroadcast(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT)
                }
                else -> { /* Show audio player */
                    val notificationIntent = Intent(this, StartActivity::class.java)
                    PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT)
                }
            }
        }

    @Volatile
    private var isForeground = false

    private var currentWidgetCover: String? = null

    val isPlaying: Boolean
        @MainThread
        get() = playlistManager.player.isPlaying()

    val isSeekable: Boolean
        @MainThread
        get() = playlistManager.player.seekable

    val isPausable: Boolean
        @MainThread
        get() = playlistManager.player.pausable

    val isShuffling: Boolean
        @MainThread
        get() = playlistManager.shuffling

    var repeatType: Int
        @MainThread
        get() = playlistManager.repeating
        @MainThread
        set(repeatType) {
            playlistManager.setRepeatType(repeatType)
            publishState()
        }

    val isVideoPlaying: Boolean
        @MainThread
        get() = playlistManager.player.isVideoPlaying()

    val album: String?
        @MainThread
        get() {
            val media = playlistManager.getCurrentMedia()
            return if (media != null) MediaUtils.getMediaAlbum(this@PlaybackService, media) else null
        }

    val artist: String?
        @MainThread
        get() {
            val media = playlistManager.getCurrentMedia()
260
261
            return if (media != null) media.nowPlaying
                    ?: MediaUtils.getMediaArtist(this@PlaybackService, media)
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
            else null
        }

    val artistPrev: String?
        @MainThread
        get() {
            val prev = playlistManager.getPrevMedia()
            return if (prev != null) MediaUtils.getMediaArtist(this@PlaybackService, prev) else null
        }

    val artistNext: String?
        @MainThread
        get() {
            val next = playlistManager.getNextMedia()
            return if (next != null) MediaUtils.getMediaArtist(this@PlaybackService, next) else null
        }

    val title: String?
        @MainThread
        get() {
            val media = playlistManager.getCurrentMedia()
            return if (media != null) if (media.nowPlaying != null) media.nowPlaying else media.title else null
        }

    val titlePrev: String?
        @MainThread
        get() {
            val prev = playlistManager.getPrevMedia()
            return prev?.title
        }

    val titleNext: String?
        @MainThread
        get() {
            val next = playlistManager.getNextMedia()
            return next?.title
        }

    val coverArt: String?
        @MainThread
        get() {
            val media = playlistManager.getCurrentMedia()
            return media?.artworkMrl
        }

    val prevCoverArt: String?
        @MainThread
        get() {
            val prev = playlistManager.getPrevMedia()
            return prev?.artworkMrl
        }

    val nextCoverArt: String?
        @MainThread
        get() {
            val next = playlistManager.getNextMedia()
            return next?.artworkMrl
        }

    var time: Long
        @MainThread
323
        get() = playlistManager.player.getCurrentTime()
324
        @MainThread
325
326
327
328
        set(time) {
            playlistManager.player.setTime(time)
            publishState(time)
        }
329
330
331

    val length: Long
        @MainThread
332
        get() = playlistManager.player.getLength()
333

334
    val lastStats: IMedia.Stats?
335
336
337
338
339
340
341
342
343
        get() = playlistManager.player.previousMediaStats

    val isPlayingPopup: Boolean
        @MainThread
        get() = popupManager != null

    val mediaListSize: Int
        get() = playlistManager.getMediaListSize()

344
    val media: List<MediaWrapper>
345
        @MainThread
346
        get() = playlistManager.getMediaList()
347
348
349
350

    val mediaLocations: List<String>
        @MainThread
        get() {
351
            return mutableListOf<String>().apply { for (mw in playlistManager.getMediaList()) add(mw.location) }
352
353
354
355
356
357
358
359
360
361
        }

    val currentMediaLocation: String?
        @MainThread
        get() = playlistManager.getCurrentMedia()?.location

    val currentMediaPosition: Int
        @MainThread
        get() = playlistManager.currentIndex

362
    val currentMediaWrapper: MediaWrapper?
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
        @MainThread
        get() = this@PlaybackService.playlistManager.getCurrentMedia()

    val rate: Float
        @MainThread
        get() = playlistManager.player.getRate()

    val titles: Array<out MediaPlayer.Title>?
        @MainThread
        get() = playlistManager.player.getTitles()

    var chapterIdx: Int
        @MainThread
        get() = playlistManager.player.getChapterIdx()
        @MainThread
        set(chapter) = playlistManager.player.setChapterIdx(chapter)

    var titleIdx: Int
        @MainThread
        get() = playlistManager.player.getTitleIdx()
        @MainThread
        set(title) = playlistManager.player.setTitleIdx(title)

    val volume: Int
        @MainThread
        get() = playlistManager.player.getVolume()

    val audioTracksCount: Int
        @MainThread
        get() = playlistManager.player.getAudioTracksCount()

    val audioTracks: Array<out MediaPlayer.TrackDescription>?
        @MainThread
        get() = playlistManager.player.getAudioTracks()

    val audioTrack: Int
        @MainThread
        get() = playlistManager.player.getAudioTrack()

    val videoTracksCount: Int
        @MainThread
        get() = if (hasMedia()) playlistManager.player.getVideoTracksCount() else 0

    val videoTracks: Array<out MediaPlayer.TrackDescription>?
        @MainThread
        get() = playlistManager.player.getVideoTracks()

410
    val currentVideoTrack: IMedia.VideoTrack?
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
        @MainThread
        get() = playlistManager.player.getCurrentVideoTrack()

    val videoTrack: Int
        @MainThread
        get() = playlistManager.player.getVideoTrack()

    val spuTracks: Array<out MediaPlayer.TrackDescription>?
        @MainThread
        get() = playlistManager.player.getSpuTracks()

    val spuTrack: Int
        @MainThread
        get() = playlistManager.player.getSpuTrack()

    val spuTracksCount: Int
        @MainThread
        get() = playlistManager.player.getSpuTracksCount()

    val audioDelay: Long
        @MainThread
        get() = playlistManager.player.getAudioDelay()

    val spuDelay: Long
        @MainThread
        get() = playlistManager.player.getSpuDelay()

    interface Callback {
        fun update()
440
        fun onMediaEvent(event: IMedia.Event)
441
442
443
444
445
446
447
448
        fun onMediaPlayerEvent(event: MediaPlayer.Event)
    }

    private inner class LocalBinder : Binder() {
        internal val service: PlaybackService
            get() = this@PlaybackService
    }

449
    override fun attachBaseContext(newBase: Context?) {
Geoffrey Métais's avatar
Geoffrey Métais committed
450
        super.attachBaseContext(newBase?.getContextWithLocale(AppContextProvider.locale))
451
452
453
    }

    override fun getApplicationContext(): Context {
Geoffrey Métais's avatar
Geoffrey Métais committed
454
        return super.getApplicationContext().getContextWithLocale(AppContextProvider.locale)
455
456
    }

457
    @RequiresApi(Build.VERSION_CODES.O)
458
    override fun onCreate() {
459
        dispatcher.onServicePreSuperOnCreate()
460
        forceForeground()
461
        super.onCreate()
462
463
        setupScope()
        NotificationHelper.createNotificationChannels(applicationContext)
464
        settings = Settings.getInstance(this)
465
        playlistManager = PlaylistManager(this)
466
        Util.checkCpuCompatibility(this)
467

468
        medialibrary = Medialibrary.getInstance()
469
470
471
472
473
474
475
476
477

        detectHeadset = settings.getBoolean("enable_headset_detection", true)

        // Make sure the audio player will acquire a wake-lock while playing. If we don't do
        // that, the CPU might go to sleep while the song is playing, causing playback to stop.
        val pm = applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager
        wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG)

        updateHasWidget()
478
        if (!this::mediaSession.isInitialized) initMediaSession()
479
480
481
482
483
484
485
486

        val filter = IntentFilter().apply {
            priority = Integer.MAX_VALUE
            addAction(VLCAppWidgetProvider.ACTION_WIDGET_INIT)
            addAction(VLCAppWidgetProvider.ACTION_WIDGET_ENABLED)
            addAction(VLCAppWidgetProvider.ACTION_WIDGET_DISABLED)
            addAction(Intent.ACTION_HEADSET_PLUG)
            addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
Habib Kazemi's avatar
Habib Kazemi committed
487
            addAction(ACTION_CAR_MODE_EXIT)
488
            addAction(SLEEP_INTENT)
489
490
491
492
        }
        registerReceiver(receiver, filter)

        keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
493
        renderer.observe(this, Observer { setRenderer(it) })
494
495
        restartPlayer.observe(this, Observer { restartMediaPlayer() })
        headSetDetection.observe(this, Observer { detectHeadset(it) })
496
        equalizer.observe(this, Observer { setEqualizer(it) })
497
        serviceFlow.value = this
498
499
    }

500
    private fun setupScope() {
501
        cbActor = lifecycleScope.actor(capacity = Channel.UNLIMITED) {
502
503
504
505
506
507
508
509
510
511
512
513
514
            for (update in channel) when (update) {
                CbUpdate -> for (callback in callbacks) callback.update()
                is CbMediaEvent -> for (callback in callbacks) callback.onMediaEvent(update.event)
                is CbMediaPlayerEvent -> for (callback in callbacks) callback.onMediaPlayerEvent(update.event)
                is CbRemove -> callbacks.remove(update.cb)
                is CbAdd -> callbacks.add(update.cb)
                ShowNotification -> showNotificationInternal()
                is HideNotification -> hideNotificationInternal(update.remove)
                UpdateMeta -> updateMetadataInternal()
            }
        }
    }

515
    private fun updateHasWidget() {
516
        val manager = AppWidgetManager.getInstance(this) ?: return
517
518
519
520
521
522
523
524
        widget = when {
            manager.getAppWidgetIds(ComponentName(this, VLCAppWidgetProviderWhite::class.java)).isNotEmpty() -> 1
            manager.getAppWidgetIds(ComponentName(this, VLCAppWidgetProviderBlack::class.java)).isNotEmpty() -> 2
            else -> 0
        }
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
525
        forceForeground()
Geoffrey Métais's avatar
Geoffrey Métais committed
526
        dispatcher.onServicePreSuperOnStart()
527
        setupScope()
528
529
        when (intent?.action) {
            Intent.ACTION_MEDIA_BUTTON -> {
530
                if (AndroidDevices.hasTsp || AndroidDevices.hasPlayServices) MediaButtonReceiver.handleIntent(mediaSession, intent)
531
            }
Habib Kazemi's avatar
Habib Kazemi committed
532
533
534
            ACTION_REMOTE_PLAYPAUSE,
            ACTION_REMOTE_PLAY,
            ACTION_REMOTE_LAST_PLAYLIST -> {
Geoffrey Métais's avatar
Geoffrey Métais committed
535
536
537
538
                if (playlistManager.hasCurrentMedia()) {
                    if (isPlaying) pause()
                    else play()
                } else loadLastAudioPlaylist()
539
            }
Geoffrey Métais's avatar
Geoffrey Métais committed
540
541
542
            ACTION_REMOTE_BACKWARD -> previous(false)
            ACTION_REMOTE_FORWARD -> next()
            ACTION_REMOTE_STOP -> stop()
Habib Kazemi's avatar
Habib Kazemi committed
543
            ACTION_PLAY_FROM_SEARCH -> {
544
                if (!this::mediaSession.isInitialized) initMediaSession()
545
                val extras = intent.getBundleExtra(EXTRA_SEARCH_BUNDLE)
546
547
548
                mediaSession.controller.transportControls
                        .playFromSearch(extras.getString(SearchManager.QUERY), extras)
            }
Geoffrey Métais's avatar
Geoffrey Métais committed
549
550
551
            ACTION_REMOTE_SWITCH_VIDEO -> {
                removePopup()
                if (hasMedia()) {
552
                    currentMediaWrapper!!.removeFlags(MediaWrapper.MEDIA_FORCE_AUDIO)
Geoffrey Métais's avatar
Geoffrey Métais committed
553
554
555
                    playlistManager.switchToVideo()
                }
            }
556
557
558
559
        }
        return Service.START_NOT_STICKY
    }

560
    override fun onTaskRemoved(rootIntent: Intent) {
561
        if (settings.getBoolean("audio_task_removed", false)) stopService(Intent(applicationContext, PlaybackService.javaClass))
562
563
    }

564
    override fun onDestroy() {
565
        serviceFlow.value = null
566
        dispatcher.onServicePreSuperOnDestroy()
567
568
569
570
        super.onDestroy()
        handler.removeCallbacksAndMessages(null)
        if (this::mediaSession.isInitialized) mediaSession.release()
        //Call it once mediaSession is null, to not publish playback state
571
        stop(systemExit = true)
572
573
574
575
576
577

        unregisterReceiver(receiver)
        playlistManager.onServiceDestroyed()
    }

    override fun onBind(intent: Intent): IBinder? {
578
        dispatcher.onServicePreSuperOnBind()
579
        return if (SERVICE_INTERFACE == intent.action) super.onBind(intent) else binder
580
581
    }

582
    val vout: IVLCVout?
583
584
585
586
        get() {
            return playlistManager.player.getVout()
        }

587
    @TargetApi(Build.VERSION_CODES.O)
588
    private fun forceForeground() {
589
        if (!AndroidUtil.isOOrLater || isForeground) return
590
        val ctx = applicationContext
591
        val stopped = PlayerController.playbackState == PlaybackStateCompat.STATE_STOPPED
592
        val notification = if (this::notification.isInitialized && !stopped) notification
593
594
595
        else {
            val pi = if (::playlistManager.isInitialized) sessionPendingIntent else null
            NotificationHelper.createPlaybackNotification(ctx, false,
596
                ctx.resources.getString(R.string.loading), "", "", null,
597
598
                false, true, null, pi)
        }
599
600
        startForeground(3, notification)
        isForeground = true
601
        if (stopped) lifecycleScope.launch { hideNotification(true) }
602
603
    }

604
    private fun sendStartSessionIdIntent() {
Nicolas Pomepuy's avatar
Nicolas Pomepuy committed
605
        val sessionId = VLCOptions.audiotrackSessionId
606
607
608
609
610
611
612
613
614
615
616
        if (sessionId == 0) return

        val intent = Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION)
        intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, sessionId)
        intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, packageName)
        if (isVideoPlaying) intent.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MOVIE)
        else intent.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC)
        sendBroadcast(intent)
    }

    private fun sendStopSessionIdIntent() {
Nicolas Pomepuy's avatar
Nicolas Pomepuy committed
617
        val sessionId = VLCOptions.audiotrackSessionId
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
        if (sessionId == 0) return

        val intent = Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION)
        intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, sessionId)
        intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, packageName)
        sendBroadcast(intent)
    }

    fun setBenchmark() {
        playlistManager.isBenchmark = true
    }

    fun setHardware() {
        playlistManager.isHardware = true
    }

634
    fun onMediaPlayerEvent(event: MediaPlayer.Event) = mediaPlayerListener.onEvent(event)
635
636

    fun onPlaybackStopped(systemExit: Boolean) {
637
        if (!systemExit) hideNotification(isForeground)
638
        removePopup()
639
        if (wakeLock.isHeld) wakeLock.release()
640
        audioFocusHelper.changeAudioFocus(false)
641
642
643
644
645
        // We must publish state before resetting mCurrentIndex
        publishState()
        executeUpdate()
    }

646
    private fun canSwitchToVideo() = playlistManager.player.canSwitchToVideo()
647

648
    fun onMediaEvent(event: IMedia.Event) = cbActor.safeOffer(CbMediaEvent(event))
649
650

    fun executeUpdate() {
651
        cbActor.safeOffer(CbUpdate)
652
653
654
655
656
657
658
        updateWidget()
        updateMetadata()
        broadcastMetadata()
    }

    private class PlaybackServiceHandler(owner: PlaybackService) : WeakHandler<PlaybackService>(owner) {

659
660
661
        var currentToast: Toast? = null
        var nbErrors = 0

662
663
664
665
666
        override fun handleMessage(msg: Message) {
            val service = owner ?: return
            when (msg.what) {
                SHOW_TOAST -> {
                    val bundle = msg.data
667
                    var text = bundle.getString("text")
668
                    val duration = bundle.getInt("duration")
669
670
671
672
673
674
675
676
677
                    val isError = bundle.getBoolean("isError")
                    if (isError) {
                        when {
                            nbErrors > 5 -> return
                            nbErrors == 5 -> text = service.getString(R.string.playback_multiple_errors)
                        }
                        currentToast?.cancel()
                        nbErrors++
                    }
678
                    currentToast = Toast.makeText(AppContextProvider.appContext, text, duration)
679
                    currentToast?.show()
680
681
682
683
684
685
                }
                END_MEDIASESSION -> if (service::mediaSession.isInitialized) service.mediaSession.isActive = false
            }
        }
    }

686
    fun showNotification(): Boolean {
687
        notificationShowing = true
688
        return cbActor.safeOffer(ShowNotification)
689
    }
690
691
692

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    private fun showNotificationInternal() {
Geoffrey Métais's avatar
Geoffrey Métais committed
693
        if (!AndroidDevices.isAndroidTv && Settings.showTvUi) return
694
        if (isPlayingPopup || !hasRenderer() && isVideoPlaying) {
695
            hideNotificationInternal(true)
696
697
698
699
700
701
702
703
704
            return
        }
        val mw = playlistManager.getCurrentMedia()
        if (mw != null) {
            val coverOnLockscreen = settings.getBoolean("lockscreen_cover", true)
            val playing = isPlaying
            val sessionToken = mediaSession.sessionToken
            val ctx = this
            val metaData = mediaSession.controller.metadata
705
            lifecycleScope.launch(Dispatchers.Default) {
706
707
                delay(100)
                if (isPlayingPopup || !notificationShowing) return@launch
708
709
710
711
712
713
714
715
716
                try {
                    val title = if (metaData == null) mw.title else metaData.getString(MediaMetadataCompat.METADATA_KEY_TITLE)
                    val artist = if (metaData == null) mw.artist else metaData.getString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST)
                    val album = if (metaData == null) mw.album else metaData.getString(MediaMetadataCompat.METADATA_KEY_ALBUM)
                    var cover = if (coverOnLockscreen && metaData != null)
                        metaData.getBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART)
                    else
                        AudioUtil.readCoverBitmap(Uri.decode(mw.artworkMrl), 256)
                    if (cover == null || cover.isRecycled)
717
                        cover = ctx.getBitmapFromDrawable(R.drawable.ic_no_media)
718

719
                    notification = NotificationHelper.createPlaybackNotification(ctx,
720
                            canSwitchToVideo(), title, artist, album,
721
                            cover, playing, isPausable, sessionToken, sessionPendingIntent)
722
                    if (isPlayingPopup) return@launch
723
                    if (!AndroidUtil.isLolliPopOrLater || playing || audioFocusHelper.lossTransient) {
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
                        if (!isForeground) {
                            this@PlaybackService.startForeground(3, notification)
                            isForeground = true
                        } else
                            NotificationManagerCompat.from(ctx).notify(3, notification)
                    } else {
                        if (isForeground) {
                            ServiceCompat.stopForeground(this@PlaybackService, ServiceCompat.STOP_FOREGROUND_DETACH)
                            isForeground = false
                        }
                        NotificationManagerCompat.from(ctx).notify(3, notification)
                    }
                } catch (e: IllegalArgumentException) {
                    // On somme crappy firmwares, shit can happen
                    Log.e(TAG, "Failed to display notification", e)
                } catch (e: IllegalStateException) {
                    Log.e(TAG, "Failed to display notification", e)
741
742
                } catch (e: RuntimeException) {
                    Log.e(TAG, "Failed to display notification", e)
743
744
745
                } catch (e: ArrayIndexOutOfBoundsException) {
                    // Happens on Android 7.0 (Xperia L1 (G3312))
                    Log.e(TAG, "Failed to display notification", e)
746
747
748
749
                }
            }
        }
    }
750
751

    private lateinit var notification: Notification
752
753
754
755
756
757

    private fun currentMediaHasFlag(flag: Int): Boolean {
        val mw = playlistManager.getCurrentMedia()
        return mw != null && mw.hasFlag(flag)
    }

758
    private fun hideNotification(remove: Boolean): Boolean {
759
        notificationShowing = false
760
        return if (::cbActor.isInitialized) cbActor.safeOffer(HideNotification(remove)) else false
761
    }
762
763
764
765
766
767
768
769
770

    private fun hideNotificationInternal(remove: Boolean) {
        if (!isPlayingPopup && isForeground) {
            ServiceCompat.stopForeground(this@PlaybackService, if (remove) ServiceCompat.STOP_FOREGROUND_REMOVE else ServiceCompat.STOP_FOREGROUND_DETACH)
            isForeground = false
        }
        NotificationManagerCompat.from(this@PlaybackService).cancel(3)
    }

771
    fun onNewPlayback() = mediaSession.setSessionActivity(sessionPendingIntent)
772
773
774
775
776
777
778

    fun onPlaylistLoaded() {
        notifyTrackChanged()
        updateMediaQueue()
    }

    @MainThread
779
    fun pause() = playlistManager.pause()
780
781

    @MainThread
782
    fun play() = playlistManager.play()
783
784
785

    @MainThread
    @JvmOverloads
786
787
    fun stop(systemExit: Boolean = false, video: Boolean = false) {
        playlistManager.stop(systemExit, video)
788
789
790
791
792
    }

    private fun initMediaSession() {
        val mediaButtonIntent = Intent(Intent.ACTION_MEDIA_BUTTON)

793
        mediaButtonIntent.setClass(this, MediaButtonReceiver::class.java)
794
        val mbrIntent = PendingIntent.getBroadcast(this, 0, mediaButtonIntent, 0)
795
        val mbrName = ComponentName(this, MediaButtonReceiver::class.java)
796
797
798

        mediaSession = MediaSessionCompat(this, "VLC", mbrName, mbrIntent)
        mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS)
799
        mediaSession.setCallback(MediaSessionCallback(this))
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
        try {
            mediaSession.isActive = true
        } catch (e: NullPointerException) {
            // Some versions of KitKat do not support AudioManager.registerMediaButtonIntent
            // with a PendingIntent. They will throw a NullPointerException, in which case
            // they should be able to activate a MediaSessionCompat with only transport
            // controls.
            mediaSession.isActive = false
            mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS)
            mediaSession.isActive = true
        }

        sessionToken = mediaSession.sessionToken
    }

    private fun updateMetadata() {
816
        cbActor.safeOffer(UpdateMeta)
817
818
819
820
821
822
    }

    private suspend fun updateMetadataInternal() {
        val media = playlistManager.getCurrentMedia() ?: return
        if (!this::mediaSession.isInitialized) initMediaSession()
        val ctx = this
823
        val length = length
824
        val bob = withContext(Dispatchers.Default) {
825
826
827
828
            val title = media.nowPlaying ?: media.title
            val coverOnLockscreen = settings.getBoolean("lockscreen_cover", true)
            val bob = MediaMetadataCompat.Builder().apply {
                putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
829
                putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, MediaSessionBrowser.generateMediaId(media))
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
                putString(MediaMetadataCompat.METADATA_KEY_GENRE, MediaUtils.getMediaGenre(ctx, media))
                putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, media.trackNumber.toLong())
                putString(MediaMetadataCompat.METADATA_KEY_ARTIST, MediaUtils.getMediaArtist(ctx, media))
                putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, MediaUtils.getMediaReferenceArtist(ctx, media))
                putString(MediaMetadataCompat.METADATA_KEY_ALBUM, MediaUtils.getMediaAlbum(ctx, media))
                putLong(MediaMetadataCompat.METADATA_KEY_DURATION, length)
            }
            if (coverOnLockscreen) {
                val cover = AudioUtil.readCoverBitmap(Uri.decode(media.artworkMrl), 512)
                if (cover?.config != null)
                //In case of format not supported
                    bob.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, cover.copy(cover.config, false))
            }
            bob.putLong("shuffle", 1L)
            bob.putLong("repeat", repeatType.toLong())
            return@withContext bob
        }
        if (this@PlaybackService::mediaSession.isInitialized) mediaSession.setMetadata(bob.build())
    }

850
    private fun publishState(position: Long? = null) {
851
852
853
854
855
        if (!this::mediaSession.isInitialized) return
        if (AndroidDevices.isAndroidTv) handler.removeMessages(END_MEDIASESSION)
        val pscb = PlaybackStateCompat.Builder()
        var actions = PLAYBACK_BASE_ACTIONS
        val hasMedia = playlistManager.hasCurrentMedia()
856
        var time = position ?: time
857
        var state = PlayerController.playbackState
858
859
860
861
862
        when (state) {
            PlaybackStateCompat.STATE_PLAYING -> actions = actions or (PlaybackStateCompat.ACTION_PAUSE or PlaybackStateCompat.ACTION_STOP)
            PlaybackStateCompat.STATE_PAUSED -> actions = actions or (PlaybackStateCompat.ACTION_PLAY or PlaybackStateCompat.ACTION_STOP)
            else -> {
                actions = actions or PlaybackStateCompat.ACTION_PLAY
863
864
                val media = if (AndroidDevices.isAndroidTv && !AndroidUtil.isOOrLater && hasMedia) playlistManager.getCurrentMedia() else null
                if (media != null) { // Hack to show a now paying card on Android TV
865
866
867
868
869
870
871
872
873
874
875
876
                    val length = media.length
                    time = media.time
                    val progress = if (length <= 0L) 0f else time / length.toFloat()
                    if (progress < 0.95f) {
                        state = PlaybackStateCompat.STATE_PAUSED
                        handler.sendEmptyMessageDelayed(END_MEDIASESSION, 900_000L)
                    }
                }
            }
        }
        pscb.setState(state, time, playlistManager.player.getRate())
        val repeatType = playlistManager.repeating
877
        if (repeatType != PlaybackStateCompat.REPEAT_MODE_NONE || hasNext())
878
            actions = actions or PlaybackStateCompat.ACTION_SKIP_TO_NEXT
879
        if (repeatType != PlaybackStateCompat.REPEAT_MODE_NONE || hasPrevious() || isSeekable)
880
881
            actions = actions or PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
        if (isSeekable)
882
            actions = actions or PlaybackStateCompat.ACTION_FAST_FORWARD or PlaybackStateCompat.ACTION_REWIND or PlaybackStateCompat.ACTION_SEEK_TO
883
        actions = actions or PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM
884
885
        if (playlistManager.hasPlaylist()) actions = actions or PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE
        actions = actions or PlaybackStateCompat.ACTION_SET_REPEAT_MODE
886
        pscb.setActions(actions)
887
888
        mediaSession.setRepeatMode(repeatType)
        mediaSession.setShuffleMode(if (isShuffling) PlaybackStateCompat.SHUFFLE_MODE_ALL else PlaybackStateCompat.SHUFFLE_MODE_NONE)
889
        val repeatResId = if (repeatType == PlaybackStateCompat.REPEAT_MODE_ALL) R.drawable.ic_auto_repeat_pressed else if (repeatType == PlaybackStateCompat.REPEAT_MODE_ONE) R.drawable.ic_auto_repeat_one_pressed else R.drawable.ic_auto_repeat_normal
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
        if (playlistManager.hasPlaylist())
            pscb.addCustomAction("shuffle", getString(R.string.shuffle_title), if (isShuffling) R.drawable.ic_auto_shuffle_pressed else R.drawable.ic_auto_shuffle_normal)
        pscb.addCustomAction("repeat", getString(R.string.repeat_title), repeatResId)

        val mediaIsActive = state != PlaybackStateCompat.STATE_STOPPED
        val update = mediaSession.isActive != mediaIsActive
        mediaSession.setPlaybackState(pscb.build())
        mediaSession.isActive = mediaIsActive
        mediaSession.setQueueTitle(getString(R.string.music_now_playing))
        if (update) {
            if (mediaIsActive) sendStartSessionIdIntent()
            else sendStopSessionIdIntent()
        }
    }

    private fun notifyTrackChanged() {
        updateMetadata()
        updateWidget()
        broadcastMetadata()
    }

911
    fun onMediaListChanged() {
912
913
914
915
916
        executeUpdate()
        updateMediaQueue()
    }

    @MainThread
917
    fun next(force: Boolean = true) = playlistManager.next(force)
918
919

    @MainThread
920
    fun previous(force: Boolean) = playlistManager.previous(force)
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953

    @MainThread
    fun shuffle() {
        playlistManager.shuffle()
        publishState()
    }

    private fun updateWidget() {
        if (widget != 0 && !isVideoPlaying) {
            updateWidgetState()
            updateWidgetCover()
        }
    }

    private fun sendWidgetBroadcast(intent: Intent) {
        intent.component = ComponentName(this@PlaybackService, if (widget == 1) VLCAppWidgetProviderWhite::class.java else VLCAppWidgetProviderBlack::class.java)
        sendBroadcast(intent)
    }

    private fun updateWidgetState() {
        val media = playlistManager.getCurrentMedia()
        val widgetIntent = Intent(VLCAppWidgetProvider.ACTION_WIDGET_UPDATE)
        if (playlistManager.hasCurrentMedia()) {
            widgetIntent.putExtra("title", media!!.title)
            widgetIntent.putExtra("artist", if (media.isArtistUnknown!! && media.nowPlaying != null)
                media.nowPlaying
            else
                MediaUtils.getMediaArtist(this@PlaybackService, media))
        } else {
            widgetIntent.putExtra("title", getString(R.string.widget_default_text))
            widgetIntent.putExtra("artist", "")
        }
        widgetIntent.putExtra("isplaying", isPlaying)
954
        lifecycleScope.launch(Dispatchers.Default) { sendWidgetBroadcast(widgetIntent) }
955
956
957
958
959
960
961
    }

    private fun updateWidgetCover() {
        val mw = playlistManager.getCurrentMedia()
        val newWidgetCover = mw?.artworkMrl
        if (!TextUtils.equals(currentWidgetCover, newWidgetCover)) {
            currentWidgetCover = newWidgetCover
962
            lifecycleScope.launch(Dispatchers.Default) {
963
964
965
                sendWidgetBroadcast(Intent(VLCAppWidgetProvider.ACTION_WIDGET_UPDATE_COVER)
                        .putExtra("artworkMrl", newWidgetCover))
            }
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
        }
    }

    private fun updateWidgetPosition(pos: Float) {
        val mw = playlistManager.getCurrentMedia()
        if (mw == null || widget == 0 || isVideoPlaying) return
        // no more than one widget mUpdateMeta for each 1/50 of the song
        val timestamp = System.currentTimeMillis()
        if (!playlistManager.hasCurrentMedia() || timestamp - widgetPositionTimestamp < mw.length / 50)
            return
        widgetPositionTimestamp = timestamp
        sendWidgetBroadcast(Intent(VLCAppWidgetProvider.ACTION_WIDGET_UPDATE_POSITION)
                .putExtra("position", pos))
    }

    private fun broadcastMetadata() {
        val media = playlistManager.getCurrentMedia()
        if (media == null || isVideoPlaying) return
984
        if (lifecycleScope.isActive) lifecycleScope.launch(Dispatchers.Default) {
985
986
987
988
989
990
991
            sendBroadcast(Intent("com.android.music.metachanged")
                    .putExtra("track", media.title)
                    .putExtra("artist", media.artist)
                    .putExtra("album", media.album)
                    .putExtra("duration", media.length)
                    .putExtra("playing", isPlaying)
                    .putExtra("package", "org.videolan.vlc"))
992
        }
993
994
995
    }

    private fun loadLastAudioPlaylist() {
Geoffrey Métais's avatar
Geoffrey Métais committed
996
        if (!AndroidDevices.isAndroidTv) loadLastPlaylist(PLAYLIST_TYPE_AUDIO)
997
998
999
    }

    fun loadLastPlaylist(type: Int) {
Geoffrey Métais's avatar
Geoffrey Métais committed
1000
        if (!playlistManager.loadLastPlaylist(type)) stopService(Intent(applicationContext, PlaybackService::class.java))
1001
1002
    }

1003
    fun showToast(text: String, duration: Int, isError: Boolean = false) {
1004
        val msg = handler.obtainMessage().apply {
1005
1006
1007
1008
            what = SHOW_TOAST
            data = Bundle(2).apply {
                putString("text", text)
                putInt("duration", duration)
1009
                putBoolean("isError", isError)
1010
1011
            }
        }
1012
        handler.removeMessages(SHOW_TOAST)
1013
1014
1015
1016
        handler.sendMessage(msg)
    }

    @MainThread
1017
    fun canShuffle() = playlistManager.canShuffle()
1018
1019

    @MainThread
1020
    fun hasMedia() = PlaylistManager.hasMedia()
1021
1022

    @MainThread
1023
    fun hasPlaylist() = playlistManager.hasPlaylist()
1024
1025

    @MainThread
1026
    fun addCallback(cb: Callback) = cbActor.safeOffer(CbAdd(cb))
1027
1028

    @MainThread
1029
    fun removeCallback(cb: Callback) = cbActor.safeOffer(CbRemove(cb))
1030

1031
    fun restartMediaPlayer() = playlistManager.player.restart()
1032

1033
    fun saveMediaMeta() = playlistManager.saveMediaMeta()
1034

1035
    fun isValidIndex(positionInPlaylist: Int) = playlistManager.isValidPosition(positionInPlaylist)
1036
1037
1038
1039
1040
1041
1042
1043
1044

    /**
     * Loads a selection of files (a non-user-supplied collection of media)
     * into the primary or "currently playing" playlist.
     *
     * @param mediaPathList A list of locations to load
     * @param position The position to start playing at
     */
    @MainThread
1045
    private fun loadLocations(mediaPathList: List<String>, position: Int) = playlistManager.loadLocations(mediaPathList, position)
1046
1047

    @MainThread
1048
    fun loadUri(uri: Uri?) = loadLocation(uri!!.toString())
1049
1050

    @MainThread
1051
    fun loadLocation(mediaPath: String) = loadLocations(listOf(mediaPath), 0)
1052
1053

    @MainThread
1054
    fun load(mediaList: Array<MediaWrapper>?, position: Int) {
1055
        mediaList?.let { load(it.toList(), position) }
1056
1057
1058
    }

    @MainThread
Geoffrey Métais's avatar
Geoffrey Métais committed
1059
    fun load(mediaList: List<MediaWrapper>, position: Int) = lifecycleScope.launch { playlistManager.load(mediaList, position) }
1060

1061
    private fun updateMediaQueue() = lifecycleScope.launch(start = CoroutineStart.UNDISPATCHED) {
1062
        if (!this@PlaybackService::mediaSession.isInitialized) initMediaSession()
1063
1064
1065
1066
        val ctx = this@PlaybackService
        val queue = withContext(Dispatchers.Default) {
            LinkedList<MediaSessionCompat.QueueItem>().also {
                for ((position, media) in playlistManager.getMediaList().withIndex()) {
1067
                    val title: String = media.nowPlaying ?: media.title
1068
1069
                    val builder = MediaDescriptionCompat.Builder()
                    builder.setTitle(title)
1070
                            .setDescription(getMediaDescription(MediaUtils.getMediaArtist(ctx, media), MediaUtils.getMediaAlbum(ctx, media)))
1071
1072
                            .setIconBitmap(BitmapUtil.getPictureFromCache(media))
                            .setMediaUri(media.uri)
1073
                            .setMediaId(MediaSessionBrowser.generateMediaId(media))
1074
1075
1076
                    it.add(MediaSessionCompat.QueueItem(builder.build(), position.toLong()))
                }
            }
1077
        }
1078
        if (this@PlaybackService.lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) mediaSession.setQueue(queue)
1079
1080
1081
    }

    @MainThread
1082
    fun load(media: MediaWrapper) = load(listOf(media), 0)
1083
1084
1085
1086
1087
1088
1089
1090

    /**
     * Play a media from the media list (playlist)
     *
     * @param index The index of the media
     * @param flags LibVLC.MEDIA_* flags
     */
    @JvmOverloads
1091
    fun playIndex(index: Int, flags: Int = 0) {
Geoffrey Métais's avatar
Geoffrey Métais committed
1092
        lifecycleScope.launch(start = CoroutineStart.UNDISPATCHED) { playlistManager.playIndex(index, flags) }
1093
    }
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115

    @MainThread
    fun flush() {
        /* HACK: flush when activating a video track. This will force an
         * I-Frame to be displayed right away. */
        if (isSeekable) {
            val time = time
            if (time > 0)
                seek(time)
        }
    }

    /**
     * Use this function to show an URI in the audio interface WITHOUT
     * interrupting the stream.
     *
     * Mainly used by VideoPlayerActivity in response to loss of video track.
     */

    @MainThread
    fun showWithoutParse(index: Int) {
        playlistManager.setVideoTrackEnabled(false)
1116
        val media = playlistManager.getMedia(index) ?: return
1117
1118
1119
1120
1121
1122
1123
1124
        // Show an URI without interrupting/losing the current stream
        if (BuildConfig.DEBUG) Log.v(TAG, "Showing index " + index + " with playing URI " + media.uri)
        playlistManager.currentIndex = index
        notifyTrackChanged()
        PlaylistManager.showAudioPlayer.value = !isVideoPlaying
        showNotification()
    }

1125
    fun setVideoTrackEnabled(enabled: Boolean) = playlistManager.setVideoTrackEnabled(enabled)
1126

1127
    fun switchToVideo() = playlistManager.switchToVideo()
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137

    @MainThread
    fun switchToPopup(index: Int) {
        playlistManager.saveMediaMeta()
        showWithoutParse(index)
        showPopup()
    }

    @MainThread
    fun removePopup() {
1138
        popupManager?.removePopup()
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
        popupManager = null
    }

    @MainThread
    private fun showPopup() {
        if (popupManager == null) popupManager = PopupManager(this)
        popupManager!!.showPopup()
        hideNotification(true)
    }

    /**
     * Append to the current existing playlist
     */

    @MainThread
1154
    fun append(mediaList: Array<MediaWrapper>) = append(mediaList.toList())
1155
1156

    @MainThread
Geoffrey Métais's avatar
Geoffrey Métais committed
1157
    fun append(mediaList: List<MediaWrapper>) = lifecycleScope.launch(start = CoroutineStart.UNDISPATCHED) {
1158
1159
1160
1161
1162
        playlistManager.append(mediaList)
        onMediaListChanged()
    }

    @MainThread
1163
    fun append(media: MediaWrapper) = append(listOf(media))
1164
1165
1166
1167
1168
1169

    /**
     * Insert into the current existing playlist
     */

    @MainThread
1170
    fun insertNext(mediaList: Array<MediaWrapper>) = insertNext(mediaList.toList())
1171
1172

    @MainThread
1173
    private fun insertNext(mediaList: List<MediaWrapper>) {
1174
1175
1176
1177
1178
        playlistManager.insertNext(mediaList)
        onMediaListChanged()
    }

    @MainThread
1179
    fun insertNext(media: MediaWrapper) = insertNext(listOf(media))
1180
1181
1182
1183
1184

    /**
     * Move an item inside the playlist.
     */
    @MainThread
1185
    fun moveItem(positionStart: Int, positionEnd: Int) = playlistManager.moveItem(positionStart, positionEnd)
1186
1187

    @MainThread
1188
    fun insertItem(position: Int, mw: MediaWrapper) = playlistManager.insertItem(position, mw)
1189
1190

    @MainThread
1191
    fun remove(position: Int) = playlistManager.remove(position)
1192
1193

    @MainThread