AudioPlayer.kt 26.9 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
23
/*****************************************************************************
 * AudioPlayer.java
 *
 * Copyright © 2011-2014 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.gui.audio

import android.Manifest
Geoffrey Métais's avatar
Geoffrey Métais committed
24
import android.annotation.TargetApi
25
import android.arch.lifecycle.Observer
26
27
28
import android.content.Context
import android.content.SharedPreferences
import android.net.Uri
Geoffrey Métais's avatar
Geoffrey Métais committed
29
import android.os.Build
30
31
32
33
34
35
36
import android.os.Bundle
import android.os.Handler
import android.preference.PreferenceManager
import android.support.annotation.MainThread
import android.support.annotation.RequiresPermission
import android.support.design.widget.BottomSheetBehavior
import android.support.design.widget.Snackbar
37
import android.support.v4.app.Fragment
38
39
40
41
42
43
44
45
46
47
48
49
50
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.helper.ItemTouchHelper
import android.text.Editable
import android.text.TextUtils
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.SeekBar
import android.widget.SeekBar.OnSeekBarChangeListener
import kotlinx.coroutines.experimental.CoroutineStart
51
import kotlinx.coroutines.experimental.IO
52
53
54
55
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.channels.Channel
import kotlinx.coroutines.experimental.channels.actor
import kotlinx.coroutines.experimental.launch
Geoffrey Métais's avatar
Geoffrey Métais committed
56
import kotlinx.coroutines.experimental.withContext
57
58
59
60
61
62
63
import org.videolan.medialibrary.Tools
import org.videolan.medialibrary.media.MediaWrapper
import org.videolan.vlc.PlaybackService
import org.videolan.vlc.R
import org.videolan.vlc.VLCApplication
import org.videolan.vlc.databinding.AudioPlayerBinding
import org.videolan.vlc.gui.AudioPlayerContainerActivity
64
import org.videolan.vlc.gui.PlaybackServiceActivity
65
import org.videolan.vlc.gui.dialogs.AdvOptionsDialog
66
67
import org.videolan.vlc.gui.dialogs.CtxActionReceiver
import org.videolan.vlc.gui.dialogs.showContext
68
69
70
71
import org.videolan.vlc.gui.helpers.AudioUtil
import org.videolan.vlc.gui.helpers.SwipeDragItemTouchHelperCallback
import org.videolan.vlc.gui.helpers.UiTools
import org.videolan.vlc.gui.preferences.PreferencesActivity
72
import org.videolan.vlc.gui.video.VideoPlayerActivity
73
import org.videolan.vlc.gui.view.AudioMediaSwitcher.AudioMediaSwitcherListener
Geoffrey Métais's avatar
Geoffrey Métais committed
74
import org.videolan.vlc.media.ABRepeat
Habib Kazemi's avatar
Habib Kazemi committed
75
import org.videolan.vlc.util.*
76
77
import org.videolan.vlc.viewmodels.PlaybackProgress
import org.videolan.vlc.viewmodels.PlaylistModel
78

79
80
81
private const val TAG = "VLC/AudioPlayer"
private const val SEARCH_TIMEOUT_MILLIS = 5000

Geoffrey Métais's avatar
Geoffrey Métais committed
82
@Suppress("UNUSED_PARAMETER")
83
class AudioPlayer : Fragment(), PlaylistAdapter.IPlayer, TextWatcher, PlaybackService.Client.Callback {
84

85
86
87
88
    private lateinit var binding: AudioPlayerBinding
    private lateinit var playlistAdapter: PlaylistAdapter
    private lateinit var settings: SharedPreferences
    private val handler by lazy(LazyThreadSafetyMode.NONE) { Handler() }
89
    private val updateActor = actor<Unit>(UI, capacity = Channel.CONFLATED) { for (entry in channel) doUpdate() }
90
91
92
93
94
95
96
97
98
99
100
101
102
103
    private lateinit var helper: PlaybackServiceActivity.Helper
    private var service: PlaybackService? = null
    private lateinit var playlistModel: PlaylistModel

    private var showRemainingTime = false
    private var previewingSeek = false
    private var advFuncVisible = false
    private var playlistSwitchVisible = false
    private var searchVisible = false
    private var headerPlayPauseVisible = false
    private var progressBarVisible = false
    private var headerTimeVisible = false
    private var playerState = 0
    private var currentCoverArt: String? = null
104
105

    companion object {
Geoffrey Métais's avatar
Geoffrey Métais committed
106
107
        private var DEFAULT_BACKGROUND_DARKER_ID = 0
        private var DEFAULT_BACKGROUND_ID = 0
108
109
110
111
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
112
113
114
115
        savedInstanceState?.let { playerState = it.getInt("player_state")}
        playlistAdapter = PlaylistAdapter(this)
        settings = PreferenceManager.getDefaultSharedPreferences(activity)
        helper = PlaybackServiceActivity.Helper(activity, this)
116
117
118
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
119
120
        binding = AudioPlayerBinding.inflate(inflater)
        return binding.root
121
122
123
124
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
125
126
        DEFAULT_BACKGROUND_DARKER_ID = UiTools.getResourceFromAttribute(view.context, R.attr.background_default_darker)
        DEFAULT_BACKGROUND_ID = UiTools.getResourceFromAttribute(view.context, R.attr.background_default)
127
128
        binding.songsList.layoutManager = LinearLayoutManager(view.context)
        binding.songsList.adapter = playlistAdapter
Geoffrey Métais's avatar
Geoffrey Métais committed
129
        binding.audioMediaSwitcher.setAudioMediaSwitcherListener(headerMediaSwitcherListener)
130
131
        binding.coverMediaSwitcher.setAudioMediaSwitcherListener(mCoverMediaSwitcherListener)
        binding.playlistSearchText.editText?.addTextChangedListener(this)
132

133
        val callback = SwipeDragItemTouchHelperCallback(playlistAdapter)
134
        val touchHelper = ItemTouchHelper(callback)
135
        touchHelper.attachToRecyclerView(binding.songsList)
136
137

        setHeaderVisibilities(false, false, true, true, true, false)
138
        binding.fragment = this
139

140
        binding.next.setOnTouchListener(LongSeekListener(true,
141
142
                UiTools.getResourceFromAttribute(view.context, R.attr.ic_next),
                R.drawable.ic_next_pressed))
143
        binding.previous.setOnTouchListener(LongSeekListener(false,
144
145
146
                UiTools.getResourceFromAttribute(view.context, R.attr.ic_previous),
                R.drawable.ic_previous_pressed))

147
        registerForContextMenu(binding.songsList)
148
        userVisibleHint = true
149
150
        binding.showCover = settings.getBoolean("audio_player_show_cover", false)
        binding.playlistSwitch.setImageResource(UiTools.getResourceFromAttribute(view.context, if (binding.showCover) R.attr.ic_playlist else R.attr.ic_playlist_on))
151
152
    }

153
154
    override fun onStart() {
        super.onStart()
155
        helper.onStart()
156
157
    }

158
159
    override fun onStop() {
        super.onStop()
160
        helper.onStop()
161
162
    }

163
164
    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
165
        outState.putInt("player_state", playerState)
166
167
    }

168
169
170
    private val ctxReceiver : CtxActionReceiver = object : CtxActionReceiver {
        override fun onCtxAction(position: Int, option: Int) {
            when(option) {
Habib Kazemi's avatar
Habib Kazemi committed
171
172
                CTX_SET_RINGTONE -> AudioUtil.setRingtone(playlistAdapter.getItem(position), activity)
                CTX_ADD_TO_PLAYLIST -> {
173
174
175
                    val mw = playlistAdapter.getItem(position)
                    UiTools.addToPlaylist(requireActivity(), listOf(mw))
                }
Alexandre Perraud's avatar
Alexandre Perraud committed
176
                CTX_REMOVE_FROM_PLAYLIST -> view?.let {
177
178
179
180
181
182
                    val mw = playlistAdapter.getItem(position)
                    val cancelAction = Runnable { service?.insertItem(position, mw) }
                    val message = String.format(VLCApplication.getAppResources().getString(R.string.remove_playlist_item), mw.title)
                    UiTools.snackerWithCancel(it, message, null, cancelAction)
                    service?.remove(position)
                }
183
                CTX_STOP_AFTER_THIS -> service?.playlistManager?.stopAfter = position
184
185
186
187
            }
        }
    }

188
    override fun onPopupMenu(anchor: View, position: Int, media: MediaWrapper) {
189
        val activity = activity
190
        if (activity === null || position >= playlistAdapter.itemCount) return
191
        val flags = CTX_REMOVE_FROM_PLAYLIST or CTX_SET_RINGTONE or CTX_ADD_TO_PLAYLIST or CTX_STOP_AFTER_THIS
192
        showContext(activity, ctxReceiver, position, media.title, flags)
193
194
195
    }

    private fun doUpdate() {
196
        if (activity === null) return
197
        service?.apply {
198
            if (hasMedia() && !isVideoPlaying && isVisible
199
200
                    && settings.getBoolean(PreferencesActivity.VIDEO_RESTORE, false)) {
                settings.edit().putBoolean(PreferencesActivity.VIDEO_RESTORE, false).apply()
201
                currentMediaWrapper?.removeFlags(MediaWrapper.MEDIA_FORCE_AUDIO)
202
203
204
                switchToVideo()
                return
            }
205
206
        }

207
208
209
210
        binding.audioMediaSwitcher.updateMedia(service)
        binding.coverMediaSwitcher.updateMedia(service)

        binding.playlistPlayasaudioOff.visibility = if (service?.videoTracksCount ?: 0 > 0) View.VISIBLE else View.GONE
211

212
213
214
        updatePlayPause()
        updateShuffleMode()
        updateRepeatMode()
Geoffrey Métais's avatar
Geoffrey Métais committed
215
        binding.timeline.setOnSeekBarChangeListener(timelineListener)
216
217
        updateBackground()
    }
218

219
220
    private fun updatePlayPause() {
        val playing = service?.isPlaying ?: false
221
222
        val imageResId = UiTools.getResourceFromAttribute(activity, if (playing) R.attr.ic_pause else R.attr.ic_play)
        val text = getString(if (playing) R.string.pause else R.string.play)
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
        binding.playPause.setImageResource(imageResId)
        binding.playPause.contentDescription = text
        binding.headerPlayPause.setImageResource(imageResId)
        binding.headerPlayPause.contentDescription = text
    }

    private fun updateShuffleMode() {
        service?.let {
            binding.shuffle.setImageResource(UiTools.getResourceFromAttribute(activity, if (it.isShuffling) R.attr.ic_shuffle_on else R.attr.ic_shuffle))
            binding.shuffle.contentDescription = resources.getString(if (it.isShuffling) R.string.shuffle_on else R.string.shuffle)
            binding.shuffle.visibility = if (it.canShuffle()) View.VISIBLE else View.INVISIBLE
        }
    }

    private fun updateRepeatMode() {
        when (service?.repeatType) {
Habib Kazemi's avatar
Habib Kazemi committed
239
            REPEAT_ONE -> {
240
241
                binding.repeat.setImageResource(UiTools.getResourceFromAttribute(activity, R.attr.ic_repeat_one))
                binding.repeat.contentDescription = resources.getString(R.string.repeat_single)
242
            }
Habib Kazemi's avatar
Habib Kazemi committed
243
            REPEAT_ALL -> {
244
245
                binding.repeat.setImageResource(UiTools.getResourceFromAttribute(activity, R.attr.ic_repeat_all))
                binding.repeat.contentDescription = resources.getString(R.string.repeat_all)
246
247
            }
            else -> {
248
249
                binding.repeat.setImageResource(UiTools.getResourceFromAttribute(activity, R.attr.ic_repeat))
                binding.repeat.contentDescription = resources.getString(R.string.repeat)
250
251
252
253
            }
        }
    }

254
255
256
257
    private fun updateProgress(progress: PlaybackProgress) {
        binding.length.text = progress.lengthText
        binding.timeline.max = progress.length.toInt()
        binding.progressBar.max = progress.length.toInt()
258

259
260
261
262
263
264
        if (!previewingSeek) {
            val displayTime = if (showRemainingTime) Tools.millisToString(progress.time - progress.length) else progress.timeText
            binding.headerTime.text = displayTime
            binding.time.text = displayTime
            binding.timeline.progress = progress.time.toInt()
            binding.progressBar.progress = progress.time.toInt()
265
266
267
        }
    }

Geoffrey Métais's avatar
Geoffrey Métais committed
268
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
269
    private fun updateBackground() {
270
        if (settings.getBoolean("blurred_cover_background", true)) {
271
            launch(UI, CoroutineStart.UNDISPATCHED) {
272
273
274
                val mw = service?.currentMediaWrapper
                if (mw === null || TextUtils.equals(currentCoverArt, mw.artworkMrl)) return@launch
                currentCoverArt = mw.artworkMrl
275
276
277
                if (TextUtils.isEmpty(mw.artworkMrl)) {
                    setDefaultBackground()
                } else {
278
                    val blurredCover = withContext(IO) { UiTools.blurBitmap(AudioUtil.readCoverBitmap(Uri.decode(mw.artworkMrl), binding.contentLayout.width)) }
279
280
281
                    if (blurredCover !== null) {
                        val activity = activity as? AudioPlayerContainerActivity
                        if (activity === null) return@launch
282
283
284
285
286
                        binding.backgroundView.setColorFilter(UiTools.getColorFromAttribute(activity, R.attr.audio_player_background_tint))
                        binding.backgroundView.setImageBitmap(blurredCover)
                        binding.backgroundView.visibility = View.VISIBLE
                        binding.songsList.setBackgroundResource(0)
                        if (playerState == BottomSheetBehavior.STATE_EXPANDED) binding.header.setBackgroundResource(0)
287
288
289
290
291
292
293
294
295
296
                    } else setDefaultBackground()
                }
            }
        }
        if ((activity as AudioPlayerContainerActivity).isAudioPlayerExpanded)
            setHeaderVisibilities(true, true, false, false, false, true)
    }

    @MainThread
    private fun setDefaultBackground() {
297
298
299
        binding.songsList.setBackgroundResource(DEFAULT_BACKGROUND_ID)
        binding.header.setBackgroundResource(DEFAULT_BACKGROUND_ID)
        binding.backgroundView.visibility = View.INVISIBLE
300
301
302
    }

    override fun onSelectionSet(position: Int) {
303
304
        if (playerState != BottomSheetBehavior.STATE_COLLAPSED && playerState != BottomSheetBehavior.STATE_HIDDEN) {
            binding.songsList.scrollToPosition(position)
305
306
307
        }
    }

308
    override fun playItem(position: Int, item: MediaWrapper) {
309
        hideSearchField()
310
        service?.playIndex(playlistModel.getItemPosition(position, item))
311
312
    }

313
    fun onTimeLabelClick(view: View) {
314
315
        showRemainingTime = !showRemainingTime
        playlistModel.progress.value?.let { updateProgress(it) }
316
317
318
    }

    fun onPlayPauseClick(view: View) {
319
        service?.run { if (isPlaying) pause() else play() }
320
321
322
    }

    fun onStopClick(view: View): Boolean {
323
        service?.stop()
324
325
326
327
        return true
    }

    fun onNextClick(view: View) {
Geoffrey Métais's avatar
Geoffrey Métais committed
328
329
330
331
        service?.run {
            if (hasNext()) next()
            else Snackbar.make(binding.root, R.string.lastsong, Snackbar.LENGTH_SHORT).show()
        }
332
333
334
    }

    fun onPreviousClick(view: View) {
Geoffrey Métais's avatar
Geoffrey Métais committed
335
336
337
338
        service?.run {
            if (hasPrevious() || isSeekable) previous(false)
            else Snackbar.make(binding.root, R.string.firstsong, Snackbar.LENGTH_SHORT).show()
        }
339
340
341
    }

    fun onRepeatClick(view: View) {
342
343
        if (service === null) return
        when (service?.repeatType) {
Habib Kazemi's avatar
Habib Kazemi committed
344
345
346
            REPEAT_NONE -> service?.repeatType = REPEAT_ALL
            REPEAT_ALL -> service?.repeatType = REPEAT_ONE
            else -> service?.repeatType = REPEAT_NONE
347
        }
348
        updateRepeatMode()
349
350
351
    }

    fun onPlaylistSwitchClick(view: View) {
352
353
354
        binding.showCover = !binding.showCover
        settings.edit().putBoolean("audio_player_show_cover", binding.showCover).apply()
        binding.playlistSwitch.setImageResource(UiTools.getResourceFromAttribute(view.context, if (binding.showCover) R.attr.ic_playlist else R.attr.ic_playlist_on))
355
356
357
    }

    fun onShuffleClick(view: View) {
358
        service?.apply {
359
            shuffle()
360
            updateShuffleMode()
361
        }
362
363
364
    }

    fun onResumeToVideoClick(v: View) {
365
        service?.apply {
366
367
368
369
370
371
372
            currentMediaWrapper?.let {
                if (hasRenderer()) VideoPlayerActivity.startOpened(VLCApplication.getAppContext(),
                        it.uri, currentMediaPosition)
                else if (hasMedia()) {
                    it.removeFlags(MediaWrapper.MEDIA_FORCE_AUDIO)
                    switchToVideo()
                }
373
            }
374
375
376
377
378
379
        }
    }


    fun showAdvancedOptions(v: View) {
        if (!isVisible) return
380
381
382
383
384
        activity?.let {
            val advOptionsDialog = AdvOptionsDialog()
            advOptionsDialog.arguments = Bundle().apply { putInt(AdvOptionsDialog.MODE_KEY, AdvOptionsDialog.MODE_AUDIO) }
            advOptionsDialog.show(it.supportFragmentManager, "fragment_adv_options")
        }
385
386
387
388
389
    }

    private fun setHeaderVisibilities(advFuncVisible: Boolean, playlistSwitchVisible: Boolean,
                                      headerPlayPauseVisible: Boolean, progressBarVisible: Boolean,
                                      headerTimeVisible: Boolean, searchVisible: Boolean) {
390
391
392
393
394
395
        this.advFuncVisible = advFuncVisible
        this.playlistSwitchVisible = playlistSwitchVisible
        this.headerPlayPauseVisible = headerPlayPauseVisible
        this.progressBarVisible = progressBarVisible
        this.headerTimeVisible = headerTimeVisible
        this.searchVisible = searchVisible
396
397
398
399
        restoreHeaderButtonVisibilities()
    }

    private fun restoreHeaderButtonVisibilities() {
400
401
402
403
404
405
        binding.advFunction.visibility = if (advFuncVisible) View.VISIBLE else View.GONE
        binding.playlistSwitch.visibility = if (playlistSwitchVisible) View.VISIBLE else View.GONE
        binding.playlistSearch.visibility = if (searchVisible) View.VISIBLE else View.GONE
        binding.headerPlayPause.visibility = if (headerPlayPauseVisible) View.VISIBLE else View.GONE
        binding.progressBar.visibility = if (progressBarVisible) View.VISIBLE else View.GONE
        binding.headerTime.visibility = if (headerTimeVisible) View.VISIBLE else View.GONE
406
407
408
    }

    private fun hideHeaderButtons() {
409
410
411
412
413
414
        binding.advFunction.visibility = View.GONE
        binding.playlistSwitch.visibility = View.GONE
        binding.playlistSearch.visibility = View.GONE
        binding.headerPlayPause.visibility = View.GONE
        binding.progressBar.visibility = View.GONE
        binding.headerTime.visibility = View.GONE
415
416
    }

Geoffrey Métais's avatar
Geoffrey Métais committed
417
418
419
420
    fun onABRepeat(v: View) {
        service?.playlistManager?.toggleABRepeat()
    }

421
    fun onSearchClick(v: View) {
422
423
        binding.playlistSearch.visibility = View.GONE
        binding.playlistSearchText.visibility = View.VISIBLE
Geoffrey Métais's avatar
Geoffrey Métais committed
424
        binding.playlistSearchText.editText?.requestFocus()
425
        if (binding.showCover) onPlaylistSwitchClick(binding.playlistSwitch)
426
        val imm = VLCApplication.getAppContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
427
428
        imm.showSoftInput(binding.playlistSearchText.editText, InputMethodManager.SHOW_IMPLICIT)
        handler.postDelayed(hideSearchRunnable, SEARCH_TIMEOUT_MILLIS.toLong())
429
430
431
432
433
    }

    override fun beforeTextChanged(charSequence: CharSequence, start: Int, before: Int, count: Int) {}

    fun clearSearch(): Boolean {
434
        if (this::playlistModel.isInitialized) playlistModel.filter(null)
435
436
437
438
        return hideSearchField()
    }

    private fun hideSearchField(): Boolean {
439
440
        if (binding.playlistSearchText.visibility != View.VISIBLE) return false
        binding.playlistSearchText.editText?.apply {
441
442
443
444
            removeTextChangedListener(this@AudioPlayer)
            setText("")
            addTextChangedListener(this@AudioPlayer)
        }
445
446
447
        UiTools.setKeyboardVisibility(binding.playlistSearchText, false)
        binding.playlistSearch.visibility = View.VISIBLE
        binding.playlistSearchText.visibility = View.GONE
448
449
450
451
452
453
        return true
    }

    override fun onTextChanged(charSequence: CharSequence, start: Int, before: Int, count: Int) {
        val length = charSequence.length
        if (length > 1) {
454
455
            playlistModel.filter(charSequence)
            handler.removeCallbacks(hideSearchRunnable)
456
        } else if (length == 0) {
457
            playlistModel.filter(null)
458
459
460
461
462
463
            hideSearchField()
        }
    }

    override fun afterTextChanged(editable: Editable) {}

Geoffrey Métais's avatar
Geoffrey Métais committed
464
465
466
467
468
469
470
471
    private val abRepeatObserver = Observer<ABRepeat> { abr ->
        if (abr != null) binding.playlistAbRepeat.setImageResource(when {
            abr.start == -1L -> R.drawable.ic_repeat
            abr.stop == -1L -> R.drawable.ic_repeat_one
            else -> R.drawable.ic_repeat_all
        })
    }

472
    override fun onConnected(service: PlaybackService) {
473
        this.service = service
474
        playlistModel = PlaylistModel.get(this, service).apply { setup() }
475
476
477
478
479
480
        playlistModel.progress.observe(this,  Observer { it?.let { updateProgress(it) } })
        playlistModel.dataset.observe(this, Observer {
            playlistAdapter.update(it!!)
            updateActor.offer(Unit)
        })
        playlistAdapter.setService(service)
Geoffrey Métais's avatar
Geoffrey Métais committed
481
        service.playlistManager.abRepeat.observe(this, abRepeatObserver)
482
    }
483

484
    override fun onDisconnected() {
485
        playlistModel.onCleared()
Geoffrey Métais's avatar
Geoffrey Métais committed
486
        service?.playlistManager?.abRepeat?.removeObserver(abRepeatObserver)
487
        service = null
488
489
490
    }

    private inner class LongSeekListener(internal var forward: Boolean, internal var normal: Int, internal var pressed: Int) : View.OnTouchListener {
Geoffrey Métais's avatar
Geoffrey Métais committed
491
        internal var length = -1L
492

Geoffrey Métais's avatar
Geoffrey Métais committed
493
494
        internal var possibleSeek = 0
        internal var vibrated = false
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511

        @RequiresPermission(Manifest.permission.VIBRATE)
        internal var seekRunnable: Runnable = object : Runnable {
            override fun run() {
                if (!vibrated) {
                    (VLCApplication.getAppContext().getSystemService(Context.VIBRATOR_SERVICE) as android.os.Vibrator)
                            .vibrate(80)
                    vibrated = true
                }

                if (forward) {
                    if (length <= 0 || possibleSeek < length) possibleSeek += 4000
                } else {
                    if (possibleSeek > 4000) possibleSeek -= 4000
                    else if (possibleSeek <= 4000) possibleSeek = 0
                }

512
513
514
515
                binding.time.text = Tools.millisToString(if (showRemainingTime) possibleSeek - length else possibleSeek.toLong())
                binding.timeline.progress = possibleSeek
                binding.progressBar.progress = possibleSeek
                handler.postDelayed(this, 50)
516
517
518
519
            }
        }

        override fun onTouch(v: View, event: MotionEvent): Boolean {
520
            if (service === null) return false
521
522
            when (event.action) {
                MotionEvent.ACTION_DOWN -> {
523
524
525
                    (if (forward) binding.next else binding.previous).setImageResource(this.pressed)
                    possibleSeek = service?.time?.toInt() ?: 0
                    previewingSeek = true
526
                    vibrated = false
527
528
                    length = service?.length ?: 0L
                    handler.postDelayed(seekRunnable, 1000)
529
530
531
532
                    return true
                }

                MotionEvent.ACTION_UP -> {
533
534
535
                    (if (forward) binding.next else binding.previous).setImageResource(this.normal)
                    handler.removeCallbacks(seekRunnable)
                    previewingSeek = false
536
537
538
539
                    if (event.eventTime - event.downTime < 1000) {
                        if (forward) onNextClick(v) else onPreviousClick(v)
                    } else {
                        if (forward) {
540
541
                            if (possibleSeek < service?.length ?: 0L)
                                service?.time = possibleSeek.toLong()
542
543
544
545
                            else
                                onNextClick(v)
                        } else {
                            if (possibleSeek > 0)
546
                                service?.time = possibleSeek.toLong()
547
548
549
550
551
552
553
554
555
556
557
558
559
                            else
                                onPreviousClick(v)
                        }
                    }
                    return true
                }
            }
            return false
        }
    }

    private fun showPlaylistTips() {
        val activity = activity as? AudioPlayerContainerActivity
Habib Kazemi's avatar
Habib Kazemi committed
560
        activity?.showTipViewIfNeeded(R.id.audio_playlist_tips, PREF_PLAYLIST_TIPS_SHOWN)
561
562
563
    }

    fun onStateChanged(newState: Int) {
564
        playerState = newState
565
566
        when (newState) {
            BottomSheetBehavior.STATE_COLLAPSED -> {
567
                hideSearchField()
568
                binding.header.setBackgroundResource(DEFAULT_BACKGROUND_DARKER_ID)
569
570
571
                setHeaderVisibilities(false, false, true, true, true, false)
            }
            BottomSheetBehavior.STATE_EXPANDED -> {
572
                binding.header.setBackgroundResource(0)
573
574
                setHeaderVisibilities(true, true, false, false, false, true)
                showPlaylistTips()
575
                service?.apply { playlistAdapter.currentIndex = currentMediaPosition }
576
            }
577
            else -> binding.header.setBackgroundResource(0)
578
579
580
        }
    }

Geoffrey Métais's avatar
Geoffrey Métais committed
581
    private var timelineListener: OnSeekBarChangeListener = object : OnSeekBarChangeListener {
582
583
584
585
586
587

        override fun onStopTrackingTouch(seekBar: SeekBar) {}

        override fun onStartTrackingTouch(seekBar: SeekBar) {}

        override fun onProgressChanged(sb: SeekBar, progress: Int, fromUser: Boolean) {
588
            if (fromUser) service?.apply {
589
                time = progress.toLong()
590
591
                binding.time.text = Tools.millisToString(if (showRemainingTime) progress - length else progress.toLong())
                binding.headerTime.text = Tools.millisToString(progress.toLong())
592
593
594
595
            }
        }
    }

Geoffrey Métais's avatar
Geoffrey Métais committed
596
    private val headerMediaSwitcherListener = object : AudioMediaSwitcherListener {
597
598
599
600

        override fun onMediaSwitching() {}

        override fun onMediaSwitched(position: Int) {
601
602
603
604
605
606
            service?.apply {
                when (position) {
                    AudioMediaSwitcherListener.PREVIOUS_MEDIA -> previous(true)
                    AudioMediaSwitcherListener.NEXT_MEDIA ->  next()
                }
            }
607
608
        }

Geoffrey Métais's avatar
Geoffrey Métais committed
609
        override fun onTouchDown() = hideHeaderButtons()
610

Geoffrey Métais's avatar
Geoffrey Métais committed
611
        override fun onTouchUp() = restoreHeaderButtonVisibilities()
612
613
614
615
616
617
618
619
620

        override fun onTouchClick() {
            val activity = activity as AudioPlayerContainerActivity
            activity.slideUpOrDownAudioPlayer()
        }
    }

    private val mCoverMediaSwitcherListener = object : AudioMediaSwitcherListener {

621
622
623
        override fun onMediaSwitching() {
            (activity as? AudioPlayerContainerActivity)?.mBottomSheetBehavior?.lock(true)
        }
624
625

        override fun onMediaSwitched(position: Int) {
626
            service?.apply {
627
628
629
630
                when (position) {
                    AudioMediaSwitcherListener.PREVIOUS_MEDIA -> previous(true)
                    AudioMediaSwitcherListener.NEXT_MEDIA -> next()
                }
631
            }
632
            (activity as? AudioPlayerContainerActivity)?.mBottomSheetBehavior?.lock(false)
633
634
635
636
637
638
639
640
641
        }

        override fun onTouchDown() {}

        override fun onTouchUp() {}

        override fun onTouchClick() {}
    }

Geoffrey Métais's avatar
Geoffrey Métais committed
642
643
644
    private val hideSearchRunnable by lazy(LazyThreadSafetyMode.NONE) {
        Runnable {
            hideSearchField()
645
            playlistModel.filter(null)
Geoffrey Métais's avatar
Geoffrey Métais committed
646
        }
647
648
    }
}