AudioPlayer.kt 27.8 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
26
27
import android.content.Context
import android.content.SharedPreferences
import android.net.Uri
Geoffrey Métais's avatar
Geoffrey Métais committed
28
import android.os.Build
29
30
31
32
33
34
35
36
37
38
39
40
import android.os.Bundle
import android.os.Handler
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
Geoffrey Métais's avatar
Geoffrey Métais committed
41
42
import androidx.annotation.MainThread
import androidx.annotation.RequiresPermission
43
import androidx.appcompat.app.AppCompatActivity
Geoffrey Métais's avatar
Geoffrey Métais committed
44
45
46
47
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.transition.AutoTransition
import androidx.transition.TransitionManager
48
import com.google.android.material.bottomsheet.BottomSheetBehavior
Geoffrey Métais's avatar
Geoffrey Métais committed
49
50
51
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Runnable
Geoffrey Métais's avatar
Geoffrey Métais committed
52
53
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.actor
Geoffrey Métais's avatar
Geoffrey Métais committed
54
55
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
56
57
import org.videolan.medialibrary.Tools
import org.videolan.medialibrary.media.MediaWrapper
58
import org.videolan.tools.coroutineScope
59
import org.videolan.vlc.R
Geoffrey Métais's avatar
Geoffrey Métais committed
60
import org.videolan.vlc.RendererDelegate.hasRenderer
61
62
63
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
66
import org.videolan.vlc.gui.dialogs.CtxActionReceiver
import org.videolan.vlc.gui.dialogs.showContext
67
import org.videolan.vlc.gui.helpers.AudioUtil
68
import org.videolan.vlc.gui.helpers.PlayerOptionsDelegate
69
70
import org.videolan.vlc.gui.helpers.SwipeDragItemTouchHelperCallback
import org.videolan.vlc.gui.helpers.UiTools
71
import org.videolan.vlc.gui.video.VideoPlayerActivity
72
import org.videolan.vlc.gui.view.AudioMediaSwitcher.AudioMediaSwitcherListener
Geoffrey Métais's avatar
Geoffrey Métais committed
73
import org.videolan.vlc.media.ABRepeat
Geoffrey Métais's avatar
Geoffrey Métais committed
74
import org.videolan.vlc.media.PlaylistManager.Companion.hasMedia
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")
Geoffrey Métais's avatar
Geoffrey Métais committed
83
class AudioPlayer : androidx.fragment.app.Fragment(), PlaylistAdapter.IPlayer, TextWatcher {
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() }
Geoffrey Métais's avatar
Geoffrey Métais committed
89
    private val updateActor = coroutineScope.actor<Unit>(capacity = Channel.CONFLATED) { for (entry in channel) doUpdate() }
90
91
    private lateinit var helper: PlaybackServiceActivity.Helper
    private lateinit var playlistModel: PlaylistModel
92
    private lateinit var optionsDelegate: PlayerOptionsDelegate
93
94
95
96
97
98

    private var showRemainingTime = false
    private var previewingSeek = false
    private var advFuncVisible = false
    private var playlistSwitchVisible = false
    private var searchVisible = false
99
    private var searchTextVisible = false
100
    private var abVisible = false
101
102
103
104
105
    private var headerPlayPauseVisible = false
    private var progressBarVisible = false
    private var headerTimeVisible = false
    private var playerState = 0
    private var currentCoverArt: String? = null
106
107

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

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
114
115
        savedInstanceState?.let { playerState = it.getInt("player_state")}
        playlistAdapter = PlaylistAdapter(this)
116
        settings = Settings.getInstance(requireContext())
Geoffrey Métais's avatar
Geoffrey Métais committed
117
118
119
120
121
122
        playlistModel = PlaylistModel.get(this)
        playlistModel.progress.observe(this@AudioPlayer,  Observer { it?.let { updateProgress(it) } })
        playlistModel.dataset.observe(this@AudioPlayer, playlistObserver)
        playlistModel.abRepeat?.observe(this, abRepeatObserver)
        helper = PlaybackServiceActivity.Helper(activity, playlistModel)
        playlistAdapter.setModel(playlistModel)
123
124
125
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
Geoffrey Métais's avatar
Geoffrey Métais committed
126
        binding = AudioPlayerBinding.inflate(inflater)
127
        return binding.root
128
129
130
131
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
132
133
        DEFAULT_BACKGROUND_DARKER_ID = UiTools.getResourceFromAttribute(view.context, R.attr.background_default_darker)
        DEFAULT_BACKGROUND_ID = UiTools.getResourceFromAttribute(view.context, R.attr.background_default)
134
        binding.songsList.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(view.context)
135
        binding.songsList.adapter = playlistAdapter
Geoffrey Métais's avatar
Geoffrey Métais committed
136
        binding.audioMediaSwitcher.setAudioMediaSwitcherListener(headerMediaSwitcherListener)
137
138
        binding.coverMediaSwitcher.setAudioMediaSwitcherListener(mCoverMediaSwitcherListener)
        binding.playlistSearchText.editText?.addTextChangedListener(this)
139

140
        val callback = SwipeDragItemTouchHelperCallback(playlistAdapter)
141
        val touchHelper = ItemTouchHelper(callback)
142
        touchHelper.attachToRecyclerView(binding.songsList)
143

144
        setHeaderVisibilities(false, false, true, true, true, false, false)
145
        binding.fragment = this
146

147
        binding.next.setOnTouchListener(LongSeekListener(true,
148
149
                UiTools.getResourceFromAttribute(view.context, R.attr.ic_next),
                R.drawable.ic_next_pressed))
150
        binding.previous.setOnTouchListener(LongSeekListener(false,
151
152
153
                UiTools.getResourceFromAttribute(view.context, R.attr.ic_previous),
                R.drawable.ic_previous_pressed))

154
        registerForContextMenu(binding.songsList)
155
        userVisibleHint = true
156
157
        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))
158
159
    }

160
161
    override fun onStart() {
        super.onStart()
162
        helper.onStart()
163
164
    }

165
166
    override fun onStop() {
        super.onStop()
167
        helper.onStop()
168
169
    }

170
171
    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
172
        outState.putInt("player_state", playerState)
173
174
    }

175
176
177
    private val ctxReceiver : CtxActionReceiver = object : CtxActionReceiver {
        override fun onCtxAction(position: Int, option: Int) {
            when(option) {
Habib Kazemi's avatar
Habib Kazemi committed
178
179
                CTX_SET_RINGTONE -> AudioUtil.setRingtone(playlistAdapter.getItem(position), activity)
                CTX_ADD_TO_PLAYLIST -> {
180
181
182
                    val mw = playlistAdapter.getItem(position)
                    UiTools.addToPlaylist(requireActivity(), listOf(mw))
                }
Alexandre Perraud's avatar
Alexandre Perraud committed
183
                CTX_REMOVE_FROM_PLAYLIST -> view?.let {
184
                    val mw = playlistAdapter.getItem(position)
Geoffrey Métais's avatar
Geoffrey Métais committed
185
                    val cancelAction = Runnable { playlistModel.insertMedia(position, mw) }
186
187
                    val message = String.format(VLCApplication.getAppResources().getString(R.string.remove_playlist_item), mw.title)
                    UiTools.snackerWithCancel(it, message, null, cancelAction)
Geoffrey Métais's avatar
Geoffrey Métais committed
188
                    playlistModel.remove(position)
189
                }
Geoffrey Métais's avatar
Geoffrey Métais committed
190
                CTX_STOP_AFTER_THIS -> playlistModel.stopAfter(position)
191
192
193
194
            }
        }
    }

195
    override fun onPopupMenu(anchor: View, position: Int, media: MediaWrapper) {
196
        val activity = activity
197
        if (activity === null || position >= playlistAdapter.itemCount) return
198
        val flags = CTX_REMOVE_FROM_PLAYLIST or CTX_SET_RINGTONE or CTX_ADD_TO_PLAYLIST or CTX_STOP_AFTER_THIS
199
        showContext(activity, ctxReceiver, position, media.title, flags)
200
201
202
    }

    private fun doUpdate() {
Geoffrey Métais's avatar
Geoffrey Métais committed
203
204
205
206
        if (activity === null || (isVisible && playlistModel.switchToVideo())) return
        binding.playlistPlayasaudioOff.visibility = if (playlistModel.videoTrackCount > 0) View.VISIBLE else View.GONE
        binding.audioMediaSwitcher.updateMedia(playlistModel.service)
        binding.coverMediaSwitcher.updateMedia(playlistModel.service)
207

208
209
210
        updatePlayPause()
        updateShuffleMode()
        updateRepeatMode()
Geoffrey Métais's avatar
Geoffrey Métais committed
211
        binding.timeline.setOnSeekBarChangeListener(timelineListener)
212
213
        updateBackground()
    }
214

215
    private fun updatePlayPause() {
Geoffrey Métais's avatar
Geoffrey Métais committed
216
        val playing = playlistModel.playing
217
218
        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)
219
220
221
222
223
224
225
        binding.playPause.setImageResource(imageResId)
        binding.playPause.contentDescription = text
        binding.headerPlayPause.setImageResource(imageResId)
        binding.headerPlayPause.contentDescription = text
    }

    private fun updateShuffleMode() {
Geoffrey Métais's avatar
Geoffrey Métais committed
226
227
228
        binding.shuffle.setImageResource(UiTools.getResourceFromAttribute(activity, if (playlistModel.shuffling) R.attr.ic_shuffle_on else R.attr.ic_shuffle))
        binding.shuffle.contentDescription = resources.getString(if (playlistModel.shuffling) R.string.shuffle_on else R.string.shuffle)
        binding.shuffle.visibility = if (playlistModel.canShuffle) View.VISIBLE else View.INVISIBLE
229
230
231
    }

    private fun updateRepeatMode() {
Geoffrey Métais's avatar
Geoffrey Métais committed
232
        when (playlistModel.repeatType) {
Habib Kazemi's avatar
Habib Kazemi committed
233
            REPEAT_ONE -> {
234
235
                binding.repeat.setImageResource(UiTools.getResourceFromAttribute(activity, R.attr.ic_repeat_one))
                binding.repeat.contentDescription = resources.getString(R.string.repeat_single)
236
            }
Habib Kazemi's avatar
Habib Kazemi committed
237
            REPEAT_ALL -> {
238
239
                binding.repeat.setImageResource(UiTools.getResourceFromAttribute(activity, R.attr.ic_repeat_all))
                binding.repeat.contentDescription = resources.getString(R.string.repeat_all)
240
241
            }
            else -> {
242
243
                binding.repeat.setImageResource(UiTools.getResourceFromAttribute(activity, R.attr.ic_repeat))
                binding.repeat.contentDescription = resources.getString(R.string.repeat)
244
245
246
247
            }
        }
    }

248
249
250
251
    private fun updateProgress(progress: PlaybackProgress) {
        binding.length.text = progress.lengthText
        binding.timeline.max = progress.length.toInt()
        binding.progressBar.max = progress.length.toInt()
252

253
254
255
256
257
258
        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()
259
260
261
        }
    }

Geoffrey Métais's avatar
Geoffrey Métais committed
262
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
263
    private fun updateBackground() {
264
        if (settings.getBoolean("blurred_cover_background", true)) {
265
            coroutineScope.launch {
Geoffrey Métais's avatar
Geoffrey Métais committed
266
                val mw = playlistModel.currentMediaWrapper
267
268
                if (mw === null || TextUtils.equals(currentCoverArt, mw.artworkMrl)) return@launch
                currentCoverArt = mw.artworkMrl
269
270
271
                if (TextUtils.isEmpty(mw.artworkMrl)) {
                    setDefaultBackground()
                } else {
272
                    val blurredCover = withContext(Dispatchers.IO) { UiTools.blurBitmap(AudioUtil.readCoverBitmap(Uri.decode(mw.artworkMrl), binding.contentLayout.width)) }
273
274
275
                    if (blurredCover !== null) {
                        val activity = activity as? AudioPlayerContainerActivity
                        if (activity === null) return@launch
276
277
278
279
                        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)
280
                        if (playerState == com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED) binding.header.setBackgroundResource(0)
281
282
283
284
                    } else setDefaultBackground()
                }
            }
        }
285
        if ((activity as AudioPlayerContainerActivity).isAudioPlayerExpanded && !searchTextVisible)
286
            setHeaderVisibilities(true, true, false, false, false, true, true)
287
288
289
290
    }

    @MainThread
    private fun setDefaultBackground() {
291
292
293
        binding.songsList.setBackgroundResource(DEFAULT_BACKGROUND_ID)
        binding.header.setBackgroundResource(DEFAULT_BACKGROUND_ID)
        binding.backgroundView.visibility = View.INVISIBLE
294
295
296
    }

    override fun onSelectionSet(position: Int) {
297
        if (playerState != com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED && playerState != com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN) {
298
            binding.songsList.scrollToPosition(position)
299
300
301
        }
    }

302
    override fun playItem(position: Int, item: MediaWrapper) {
303
        clearSearch()
Geoffrey Métais's avatar
Geoffrey Métais committed
304
        playlistModel.play(playlistModel.getPlaylistPosition(position, item))
305
306
    }

307
    fun onTimeLabelClick(view: View) {
308
        showRemainingTime = !showRemainingTime
Geoffrey Métais's avatar
Geoffrey Métais committed
309
        playlistModel.progress?.value?.let { updateProgress(it) }
310
311
312
    }

    fun onPlayPauseClick(view: View) {
Geoffrey Métais's avatar
Geoffrey Métais committed
313
        playlistModel.togglePlayPause()
314
315
316
    }

    fun onStopClick(view: View): Boolean {
Geoffrey Métais's avatar
Geoffrey Métais committed
317
        playlistModel.stop()
318
319
320
321
        return true
    }

    fun onNextClick(view: View) {
Geoffrey Métais's avatar
Geoffrey Métais committed
322
        if (!playlistModel.next()) Snackbar.make(binding.root, R.string.lastsong, com.google.android.material.snackbar.Snackbar.LENGTH_SHORT).show()
323
324
325
    }

    fun onPreviousClick(view: View) {
Geoffrey Métais's avatar
Geoffrey Métais committed
326
        if (!playlistModel.previous()) Snackbar.make(binding.root, R.string.firstsong, com.google.android.material.snackbar.Snackbar.LENGTH_SHORT).show()
327
328
329
    }

    fun onRepeatClick(view: View) {
Geoffrey Métais's avatar
Geoffrey Métais committed
330
331
332
333
        when (playlistModel.repeatType) {
            REPEAT_NONE -> playlistModel.repeatType = REPEAT_ALL
            REPEAT_ALL -> playlistModel.repeatType = REPEAT_ONE
            else -> playlistModel.repeatType = REPEAT_NONE
334
        }
335
        updateRepeatMode()
336
337
338
    }

    fun onPlaylistSwitchClick(view: View) {
339
340
341
        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))
342
343
344
    }

    fun onShuffleClick(view: View) {
Geoffrey Métais's avatar
Geoffrey Métais committed
345
346
        playlistModel.shuffle()
        updateShuffleMode()
347
348
349
    }

    fun onResumeToVideoClick(v: View) {
Geoffrey Métais's avatar
Geoffrey Métais committed
350
351
352
353
354
355
        playlistModel.currentMediaWrapper?.let {
            if (hasRenderer()) VideoPlayerActivity.startOpened(v.context,
                    it.uri, playlistModel.currentMediaPosition)
            else if (hasMedia()) {
                it.removeFlags(MediaWrapper.MEDIA_FORCE_AUDIO)
                playlistModel.switchToVideo()
356
            }
357
358
359
360
361
362
        }
    }


    fun showAdvancedOptions(v: View) {
        if (!isVisible) return
363
364
365
366
        if (!this::optionsDelegate.isInitialized) {
            val service = playlistModel.service ?: return
            val activity = activity as? AppCompatActivity ?: return
            optionsDelegate = PlayerOptionsDelegate(activity, service)
367
        }
368
        optionsDelegate.show()
369
370
371
372
    }

    private fun setHeaderVisibilities(advFuncVisible: Boolean, playlistSwitchVisible: Boolean,
                                      headerPlayPauseVisible: Boolean, progressBarVisible: Boolean,
373
374
375
376
377
378
379
380
381
382
                                      headerTimeVisible: Boolean, searchVisible: Boolean,
                                      abVisible: Boolean, filter: Boolean = false) {
        this.advFuncVisible = !filter && advFuncVisible
        this.playlistSwitchVisible = !filter && playlistSwitchVisible
        this.headerPlayPauseVisible = !filter && headerPlayPauseVisible
        this.progressBarVisible = !filter && progressBarVisible
        this.headerTimeVisible = !filter && headerTimeVisible
        this.searchVisible = !filter && searchVisible
        this.abVisible = !filter && abVisible
        this.searchTextVisible = filter
383
384
385
386
        restoreHeaderButtonVisibilities()
    }

    private fun restoreHeaderButtonVisibilities() {
387
        binding.progressBar.visibility = if (progressBarVisible) View.VISIBLE else View.GONE
388
389
        val cl = binding.header
        TransitionManager.beginDelayedTransition(cl, AutoTransition().setDuration(200))
390
        androidx.constraintlayout.widget.ConstraintSet().apply {
391
            clone(cl)
392
393
394
395
396
397
398
399
            setVisibility(R.id.playlist_ab_repeat, if (abVisible) androidx.constraintlayout.widget.ConstraintSet.VISIBLE else androidx.constraintlayout.widget.ConstraintSet.GONE)
            setVisibility(R.id.playlist_search, if (searchVisible) androidx.constraintlayout.widget.ConstraintSet.VISIBLE else androidx.constraintlayout.widget.ConstraintSet.GONE)
            setVisibility(R.id.playlist_switch, if (playlistSwitchVisible) androidx.constraintlayout.widget.ConstraintSet.VISIBLE else androidx.constraintlayout.widget.ConstraintSet.GONE)
            setVisibility(R.id.adv_function, if (advFuncVisible) androidx.constraintlayout.widget.ConstraintSet.VISIBLE else androidx.constraintlayout.widget.ConstraintSet.GONE)
            setVisibility(R.id.header_play_pause, if (headerPlayPauseVisible) androidx.constraintlayout.widget.ConstraintSet.VISIBLE else androidx.constraintlayout.widget.ConstraintSet.GONE)
            setVisibility(R.id.header_time, if (headerTimeVisible) androidx.constraintlayout.widget.ConstraintSet.VISIBLE else androidx.constraintlayout.widget.ConstraintSet.GONE)
            setVisibility(R.id.playlist_search_text, if (searchTextVisible) androidx.constraintlayout.widget.ConstraintSet.VISIBLE else androidx.constraintlayout.widget.ConstraintSet.GONE)
            setVisibility(R.id.audio_media_switcher, if (searchTextVisible) androidx.constraintlayout.widget.ConstraintSet.GONE else androidx.constraintlayout.widget.ConstraintSet.VISIBLE)
400
401
            applyTo(cl)
        }
402
403
    }

Geoffrey Métais's avatar
Geoffrey Métais committed
404
    fun onABRepeat(v: View) {
Geoffrey Métais's avatar
Geoffrey Métais committed
405
        playlistModel.toggleABRepeat()
Geoffrey Métais's avatar
Geoffrey Métais committed
406
407
    }

408
    fun onSearchClick(v: View) {
409
        setHeaderVisibilities(false, false, false, false, false, false, false, true)
Geoffrey Métais's avatar
Geoffrey Métais committed
410
        binding.playlistSearchText.editText?.requestFocus()
411
        if (binding.showCover) onPlaylistSwitchClick(binding.playlistSwitch)
412
        val imm = v.context.applicationContext.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
413
414
        imm.showSoftInput(binding.playlistSearchText.editText, InputMethodManager.SHOW_IMPLICIT)
        handler.postDelayed(hideSearchRunnable, SEARCH_TIMEOUT_MILLIS.toLong())
415
416
417
418
    }

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

419
420
421
422
423
424
425
426
427
    fun backPressed() : Boolean {
        if (this::optionsDelegate.isInitialized && optionsDelegate.isShowing()) {
            optionsDelegate.hide()
            return true
        }
        return clearSearch()
    }

    private fun clearSearch(): Boolean {
428
        if (this::playlistModel.isInitialized) playlistModel.filter(null)
429
430
431
432
        return hideSearchField()
    }

    private fun hideSearchField(): Boolean {
433
434
        if (binding.playlistSearchText.visibility != View.VISIBLE) return false
        binding.playlistSearchText.editText?.apply {
435
436
437
438
            removeTextChangedListener(this@AudioPlayer)
            setText("")
            addTextChangedListener(this@AudioPlayer)
        }
439
        UiTools.setKeyboardVisibility(binding.playlistSearchText, false)
440
        if (playerState == com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED) setHeaderVisibilities(false, false, true, true, true, false, false)
441
        else setHeaderVisibilities(true, true, false, false, false, true, true)
442
443
444
445
446
447
        return true
    }

    override fun onTextChanged(charSequence: CharSequence, start: Int, before: Int, count: Int) {
        val length = charSequence.length
        if (length > 1) {
448
449
            playlistModel.filter(charSequence)
            handler.removeCallbacks(hideSearchRunnable)
450
        } else if (length == 0) {
451
            playlistModel.filter(null)
452
453
454
455
456
457
            hideSearchField()
        }
    }

    override fun afterTextChanged(editable: Editable) {}

Geoffrey Métais's avatar
Geoffrey Métais committed
458
459
460
461
462
463
464
465
    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
        })
    }

466
467
468
469
470
    private val playlistObserver = Observer<MutableList<MediaWrapper>> {
        playlistAdapter.update(it!!)
        updateActor.offer(Unit)
    }

Geoffrey Métais's avatar
Geoffrey Métais committed
471
472
473
    override fun onDestroy() {
        super.onDestroy()
        playlistModel.abRepeat.removeObserver(abRepeatObserver)
474
        playlistModel.dataset.removeObserver(playlistObserver)
475
        playlistModel.onCleared()
476
477
478
    }

    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
479
        internal var length = -1L
480

Geoffrey Métais's avatar
Geoffrey Métais committed
481
482
        internal var possibleSeek = 0
        internal var vibrated = false
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499

        @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
                }

500
501
502
503
                binding.time.text = Tools.millisToString(if (showRemainingTime) possibleSeek - length else possibleSeek.toLong())
                binding.timeline.progress = possibleSeek
                binding.progressBar.progress = possibleSeek
                handler.postDelayed(this, 50)
504
505
506
507
508
509
            }
        }

        override fun onTouch(v: View, event: MotionEvent): Boolean {
            when (event.action) {
                MotionEvent.ACTION_DOWN -> {
510
                    (if (forward) binding.next else binding.previous).setImageResource(this.pressed)
Geoffrey Métais's avatar
Geoffrey Métais committed
511
                    possibleSeek = playlistModel.time.toInt()
512
                    previewingSeek = true
513
                    vibrated = false
Geoffrey Métais's avatar
Geoffrey Métais committed
514
                    length = playlistModel.length
515
                    handler.postDelayed(seekRunnable, 1000)
516
517
518
519
                    return true
                }

                MotionEvent.ACTION_UP -> {
520
521
522
                    (if (forward) binding.next else binding.previous).setImageResource(this.normal)
                    handler.removeCallbacks(seekRunnable)
                    previewingSeek = false
523
524
525
526
                    if (event.eventTime - event.downTime < 1000) {
                        if (forward) onNextClick(v) else onPreviousClick(v)
                    } else {
                        if (forward) {
Geoffrey Métais's avatar
Geoffrey Métais committed
527
528
                            if (possibleSeek < playlistModel.length)
                                playlistModel.time = possibleSeek.toLong()
529
530
531
532
                            else
                                onNextClick(v)
                        } else {
                            if (possibleSeek > 0)
Geoffrey Métais's avatar
Geoffrey Métais committed
533
                                playlistModel.time = possibleSeek.toLong()
534
535
536
537
538
539
540
541
542
543
544
545
546
                            else
                                onPreviousClick(v)
                        }
                    }
                    return true
                }
            }
            return false
        }
    }

    private fun showPlaylistTips() {
        val activity = activity as? AudioPlayerContainerActivity
Habib Kazemi's avatar
Habib Kazemi committed
547
        activity?.showTipViewIfNeeded(R.id.audio_playlist_tips, PREF_PLAYLIST_TIPS_SHOWN)
548
549
550
    }

    fun onStateChanged(newState: Int) {
551
        playerState = newState
552
        when (newState) {
553
554
            BottomSheetBehavior.STATE_COLLAPSED -> {
                backPressed()
555
                binding.header.setBackgroundResource(DEFAULT_BACKGROUND_DARKER_ID)
556
                setHeaderVisibilities(false, false, true, true, true, false, false)
557
            }
558
            BottomSheetBehavior.STATE_EXPANDED -> {
559
                binding.header.setBackgroundResource(0)
560
                setHeaderVisibilities(true, true, false, false, false, true, true)
561
                showPlaylistTips()
Geoffrey Métais's avatar
Geoffrey Métais committed
562
                playlistAdapter.currentIndex = playlistModel.currentMediaPosition
563
            }
564
//            else -> binding.header.setBackgroundResource(0)
565
566
567
        }
    }

Geoffrey Métais's avatar
Geoffrey Métais committed
568
    private var timelineListener: OnSeekBarChangeListener = object : OnSeekBarChangeListener {
569
570
571
572
573
574

        override fun onStopTrackingTouch(seekBar: SeekBar) {}

        override fun onStartTrackingTouch(seekBar: SeekBar) {}

        override fun onProgressChanged(sb: SeekBar, progress: Int, fromUser: Boolean) {
Geoffrey Métais's avatar
Geoffrey Métais committed
575
576
577
            if (fromUser)  {
                playlistModel.time = progress.toLong()
                binding.time.text = Tools.millisToString(if (showRemainingTime) progress - playlistModel.length else progress.toLong())
578
                binding.headerTime.text = Tools.millisToString(progress.toLong())
579
580
581
582
            }
        }
    }

Geoffrey Métais's avatar
Geoffrey Métais committed
583
    private val headerMediaSwitcherListener = object : AudioMediaSwitcherListener {
584
585
586
587

        override fun onMediaSwitching() {}

        override fun onMediaSwitched(position: Int) {
588
                when (position) {
Geoffrey Métais's avatar
Geoffrey Métais committed
589
590
                    AudioMediaSwitcherListener.PREVIOUS_MEDIA -> playlistModel.previous(true)
                    AudioMediaSwitcherListener.NEXT_MEDIA ->  playlistModel.next()
591
                }
592
593
594
595
596
597
        }

        override fun onTouchClick() {
            val activity = activity as AudioPlayerContainerActivity
            activity.slideUpOrDownAudioPlayer()
        }
598
599
600
601

        override fun onTouchDown() {}

        override fun onTouchUp() {}
602
603
604
605
    }

    private val mCoverMediaSwitcherListener = object : AudioMediaSwitcherListener {

606
607
608
        override fun onMediaSwitching() {
            (activity as? AudioPlayerContainerActivity)?.mBottomSheetBehavior?.lock(true)
        }
609
610

        override fun onMediaSwitched(position: Int) {
Geoffrey Métais's avatar
Geoffrey Métais committed
611
612
613
            when (position) {
                AudioMediaSwitcherListener.PREVIOUS_MEDIA -> playlistModel.previous(true)
                AudioMediaSwitcherListener.NEXT_MEDIA -> playlistModel.next()
614
            }
615
            (activity as? AudioPlayerContainerActivity)?.mBottomSheetBehavior?.lock(false)
616
617
618
619
620
621
622
623
624
        }

        override fun onTouchDown() {}

        override fun onTouchUp() {}

        override fun onTouchClick() {}
    }

Geoffrey Métais's avatar
Geoffrey Métais committed
625
626
627
    private val hideSearchRunnable by lazy(LazyThreadSafetyMode.NONE) {
        Runnable {
            hideSearchField()
628
            playlistModel.filter(null)
Geoffrey Métais's avatar
Geoffrey Métais committed
629
        }
630
631
    }
}