AudioPlayer.kt 28.1 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
import android.content.Context
import android.content.SharedPreferences
27
import android.content.res.Configuration
28
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
37
38
39
40
41
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
42
43
import androidx.annotation.MainThread
import androidx.annotation.RequiresPermission
44
import androidx.appcompat.app.AppCompatActivity
45
46
import androidx.constraintlayout.widget.ConstraintSet
import androidx.fragment.app.Fragment
Geoffrey Métais's avatar
Geoffrey Métais committed
47
48
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.ItemTouchHelper
49
import androidx.recyclerview.widget.LinearLayoutManager
Geoffrey Métais's avatar
Geoffrey Métais committed
50
51
import androidx.transition.AutoTransition
import androidx.transition.TransitionManager
52
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
53
import com.google.android.material.bottomsheet.BottomSheetBehavior
Geoffrey Métais's avatar
Geoffrey Métais committed
54
import com.google.android.material.snackbar.Snackbar
Geoffrey Métais's avatar
Geoffrey Métais committed
55
import kotlinx.coroutines.*
Geoffrey Métais's avatar
Geoffrey Métais committed
56
57
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.actor
58
59
import org.videolan.medialibrary.Tools
import org.videolan.medialibrary.media.MediaWrapper
60
import org.videolan.tools.isStarted
61
import org.videolan.vlc.PlaybackService
62
63
64
65
import org.videolan.vlc.R
import org.videolan.vlc.VLCApplication
import org.videolan.vlc.databinding.AudioPlayerBinding
import org.videolan.vlc.gui.AudioPlayerContainerActivity
66
67
import org.videolan.vlc.gui.dialogs.CtxActionReceiver
import org.videolan.vlc.gui.dialogs.showContext
68
import org.videolan.vlc.gui.helpers.*
69
import org.videolan.vlc.gui.video.VideoPlayerActivity
70
import org.videolan.vlc.gui.view.AudioMediaSwitcher.AudioMediaSwitcherListener
Geoffrey Métais's avatar
Geoffrey Métais committed
71
import org.videolan.vlc.media.PlaylistManager.Companion.hasMedia
Habib Kazemi's avatar
Habib Kazemi committed
72
import org.videolan.vlc.util.*
73
74
import org.videolan.vlc.viewmodels.PlaybackProgress
import org.videolan.vlc.viewmodels.PlaylistModel
75

76
private const val TAG = "VLC/AudioPlayer"
77
private const val SEARCH_TIMEOUT_MILLIS = 10000L
78

79
@ObsoleteCoroutinesApi
Geoffrey Métais's avatar
Geoffrey Métais committed
80
@ExperimentalCoroutinesApi
Geoffrey Métais's avatar
Geoffrey Métais committed
81
@Suppress("UNUSED_PARAMETER")
82
class AudioPlayer : Fragment(), PlaylistAdapter.IPlayer, TextWatcher, CoroutineScope by MainScope() {
83

84
85
86
87
    private lateinit var binding: AudioPlayerBinding
    private lateinit var playlistAdapter: PlaylistAdapter
    private lateinit var settings: SharedPreferences
    private val handler by lazy(LazyThreadSafetyMode.NONE) { Handler() }
88
    private val updateActor = actor<Unit>(capacity = Channel.CONFLATED) { for (entry in channel) doUpdate() }
89
    private lateinit var playlistModel: PlaylistModel
90
    private lateinit var optionsDelegate: PlayerOptionsDelegate
91
92
93
94
95
96

    private var showRemainingTime = false
    private var previewingSeek = false
    private var advFuncVisible = false
    private var playlistSwitchVisible = false
    private var searchVisible = false
97
    private var searchTextVisible = false
98
99
100
101
102
    private var headerPlayPauseVisible = false
    private var progressBarVisible = false
    private var headerTimeVisible = false
    private var playerState = 0
    private var currentCoverArt: String? = null
103
104
105
106
    private lateinit var pauseToPlay: AnimatedVectorDrawableCompat
    private lateinit var playToPause: AnimatedVectorDrawableCompat
    private lateinit var pauseToPlaySmall: AnimatedVectorDrawableCompat
    private lateinit var playToPauseSmall: AnimatedVectorDrawableCompat
107
108

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

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

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

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

142
        val callback = SwipeDragItemTouchHelperCallback(playlistAdapter)
143
        val touchHelper = ItemTouchHelper(callback)
144
        touchHelper.attachToRecyclerView(binding.songsList)
145

146
        setHeaderVisibilities(false, false, true, true, true, false)
147
        binding.fragment = this
148

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

156
        registerForContextMenu(binding.songsList)
157
        userVisibleHint = true
158
159
        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))
160
        binding.timeline.setOnSeekBarChangeListener(timelineListener)
161
162
163
164
165
166

        //For resizing purpose, we have to cache this twice even if it's from the same resource
        playToPause = AnimatedVectorDrawableCompat.create(requireActivity(), R.drawable.anim_play_pause)!!
        pauseToPlay = AnimatedVectorDrawableCompat.create(requireActivity(), R.drawable.anim_pause_play)!!
        playToPauseSmall = AnimatedVectorDrawableCompat.create(requireActivity(), R.drawable.anim_play_pause)!!
        pauseToPlaySmall = AnimatedVectorDrawableCompat.create(requireActivity(), R.drawable.anim_pause_play)!!
167
168
    }

169
170
171
172
173
    override fun onResume() {
        onStateChanged(playerState)
        super.onResume()
    }

174
175
    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
176
        outState.putInt("player_state", playerState)
177
        outState.putBoolean("was_playing", wasPlaying)
178
179
    }

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

Nicolas Pomepuy's avatar
Nicolas Pomepuy committed
200
    override fun onPopupMenu(view: View, position: Int, item: MediaWrapper?) {
201
        val activity = activity
202
        if (activity === null || position >= playlistAdapter.itemCount) return
203
        val flags = CTX_REMOVE_FROM_PLAYLIST or CTX_SET_RINGTONE or CTX_ADD_TO_PLAYLIST or CTX_STOP_AFTER_THIS
Nicolas Pomepuy's avatar
Nicolas Pomepuy committed
204
        showContext(activity, ctxReceiver, position, item?.title ?: "", flags)
205
206
207
    }

    private fun doUpdate() {
Geoffrey Métais's avatar
Geoffrey Métais committed
208
209
210
211
        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)
212

213
214
215
216
217
        updatePlayPause()
        updateShuffleMode()
        updateRepeatMode()
        updateBackground()
    }
218

219
    private var wasPlaying = true
220
    private fun updatePlayPause() {
Geoffrey Métais's avatar
Geoffrey Métais committed
221
        val playing = playlistModel.playing
222
        val text = getString(if (playing) R.string.pause else R.string.play)
223
224
225
226
227
228
229
230
231
232

        val drawable = if (playing) playToPause else pauseToPlay
        val drawableSmall = if (playing) playToPauseSmall else pauseToPlaySmall
        binding.playPause.setImageDrawable(drawable)
        binding.headerPlayPause.setImageDrawable(drawableSmall)
        if (playing != wasPlaying) {
            drawable.start()
            drawableSmall.start()
        }

233
234
        binding.playPause.contentDescription = text
        binding.headerPlayPause.contentDescription = text
235
        wasPlaying = playing
236
237
    }

238
    private var wasShuffling = false
239
    private fun updateShuffleMode() {
240
        binding.shuffle.visibility = if (playlistModel.canShuffle) View.VISIBLE else View.INVISIBLE
241
242
        val shuffling = playlistModel.shuffling
        if (wasShuffling == shuffling) return
Nicolas Pomepuy's avatar
Nicolas Pomepuy committed
243
        binding.shuffle.setImageResource(UiTools.getResourceFromAttribute(requireActivity(), if (shuffling) R.attr.ic_shuffle_on else R.attr.ic_shuffle))
244
245
        binding.shuffle.contentDescription = resources.getString(if (shuffling) R.string.shuffle_on else R.string.shuffle)
        wasShuffling = shuffling
246
247
    }

248
    private var previousRepeatType = -1
249
    private fun updateRepeatMode() {
250
251
252
        val repeatType = playlistModel.repeatType
        if (previousRepeatType == repeatType) return
        when (repeatType) {
Habib Kazemi's avatar
Habib Kazemi committed
253
            REPEAT_ONE -> {
Nicolas Pomepuy's avatar
Nicolas Pomepuy committed
254
                binding.repeat.setImageResource(UiTools.getResourceFromAttribute(requireActivity(), R.attr.ic_repeat_one))
255
                binding.repeat.contentDescription = resources.getString(R.string.repeat_single)
256
            }
Habib Kazemi's avatar
Habib Kazemi committed
257
            REPEAT_ALL -> {
Nicolas Pomepuy's avatar
Nicolas Pomepuy committed
258
                binding.repeat.setImageResource(UiTools.getResourceFromAttribute(requireActivity(), R.attr.ic_repeat_all))
259
                binding.repeat.contentDescription = resources.getString(R.string.repeat_all)
260
261
            }
            else -> {
Nicolas Pomepuy's avatar
Nicolas Pomepuy committed
262
                binding.repeat.setImageResource(UiTools.getResourceFromAttribute(requireActivity(), R.attr.ic_repeat))
263
                binding.repeat.contentDescription = resources.getString(R.string.repeat)
264
265
            }
        }
266
        previousRepeatType = repeatType
267
268
    }

269
270
271
272
    private fun updateProgress(progress: PlaybackProgress) {
        binding.length.text = progress.lengthText
        binding.timeline.max = progress.length.toInt()
        binding.progressBar.max = progress.length.toInt()
273

274
275
276
277
278
279
        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()
280
281
282
        }
    }

Geoffrey Métais's avatar
Geoffrey Métais committed
283
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
284
    private fun updateBackground() {
285
        if (settings.getBoolean("blurred_cover_background", true)) {
286
            launch {
Geoffrey Métais's avatar
Geoffrey Métais committed
287
                val mw = playlistModel.currentMediaWrapper
288
                if (!isStarted() || mw === null || TextUtils.equals(currentCoverArt, mw.artworkMrl)) return@launch
289
                currentCoverArt = mw.artworkMrl
290
291
292
                if (TextUtils.isEmpty(mw.artworkMrl)) {
                    setDefaultBackground()
                } else {
293
294
                    val width = if (binding.contentLayout.width > 0) binding.contentLayout.width else activity?.getScreenWidth()
                            ?: return@launch
295
                    val blurredCover = withContext(Dispatchers.IO) { UiTools.blurBitmap(AudioUtil.readCoverBitmap(Uri.decode(mw.artworkMrl), width)) }
296
                    if (!isStarted()) return@launch
297
                    if (blurredCover !== null) {
298
                        val activity = activity as? AudioPlayerContainerActivity ?: return@launch
299
300
301
302
                        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)
303
                        if (playerState == com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED) binding.header.setBackgroundResource(0)
304
305
306
307
308
309
310
311
                    } else setDefaultBackground()
                }
            }
        }
    }

    @MainThread
    private fun setDefaultBackground() {
312
313
314
        binding.songsList.setBackgroundResource(DEFAULT_BACKGROUND_ID)
        binding.header.setBackgroundResource(DEFAULT_BACKGROUND_ID)
        binding.backgroundView.visibility = View.INVISIBLE
315
316
317
    }

    override fun onSelectionSet(position: Int) {
318
        if (playerState != com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED && playerState != com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN) {
319
            binding.songsList.scrollToPosition(position)
320
321
322
        }
    }

323
    override fun playItem(position: Int, item: MediaWrapper) {
324
        clearSearch()
Geoffrey Métais's avatar
Geoffrey Métais committed
325
        playlistModel.play(playlistModel.getPlaylistPosition(position, item))
326
327
    }

328
    fun onTimeLabelClick(view: View) {
329
        showRemainingTime = !showRemainingTime
330
        playlistModel.progress.value?.let { updateProgress(it) }
331
332
333
    }

    fun onPlayPauseClick(view: View) {
Geoffrey Métais's avatar
Geoffrey Métais committed
334
        playlistModel.togglePlayPause()
335
336
337
    }

    fun onStopClick(view: View): Boolean {
Geoffrey Métais's avatar
Geoffrey Métais committed
338
        playlistModel.stop()
339
340
341
342
        return true
    }

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

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

    fun onRepeatClick(view: View) {
Geoffrey Métais's avatar
Geoffrey Métais committed
351
352
353
354
        when (playlistModel.repeatType) {
            REPEAT_NONE -> playlistModel.repeatType = REPEAT_ALL
            REPEAT_ALL -> playlistModel.repeatType = REPEAT_ONE
            else -> playlistModel.repeatType = REPEAT_NONE
355
        }
356
        updateRepeatMode()
357
358
359
    }

    fun onPlaylistSwitchClick(view: View) {
360
361
362
        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))
363
364
365
    }

    fun onShuffleClick(view: View) {
Geoffrey Métais's avatar
Geoffrey Métais committed
366
367
        playlistModel.shuffle()
        updateShuffleMode()
368
369
370
    }

    fun onResumeToVideoClick(v: View) {
Geoffrey Métais's avatar
Geoffrey Métais committed
371
        playlistModel.currentMediaWrapper?.let {
372
            if (PlaybackService.hasRenderer()) VideoPlayerActivity.startOpened(v.context,
Geoffrey Métais's avatar
Geoffrey Métais committed
373
374
375
376
                    it.uri, playlistModel.currentMediaPosition)
            else if (hasMedia()) {
                it.removeFlags(MediaWrapper.MEDIA_FORCE_AUDIO)
                playlistModel.switchToVideo()
377
            }
378
379
380
381
382
        }
    }

    fun showAdvancedOptions(v: View) {
        if (!isVisible) return
383
384
385
386
        if (!this::optionsDelegate.isInitialized) {
            val service = playlistModel.service ?: return
            val activity = activity as? AppCompatActivity ?: return
            optionsDelegate = PlayerOptionsDelegate(activity, service)
387
        }
388
        optionsDelegate.show(PlayerOptionType.ADVANCED)
389
390
391
392
    }

    private fun setHeaderVisibilities(advFuncVisible: Boolean, playlistSwitchVisible: Boolean,
                                      headerPlayPauseVisible: Boolean, progressBarVisible: Boolean,
393
                                      headerTimeVisible: Boolean, searchVisible: Boolean,
394
                                      filter: Boolean = false) {
395
        this.advFuncVisible = !filter && advFuncVisible
396
        this.playlistSwitchVisible = !filter && playlistSwitchVisible && resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE
397
398
399
400
401
        this.headerPlayPauseVisible = !filter && headerPlayPauseVisible
        this.progressBarVisible = !filter && progressBarVisible
        this.headerTimeVisible = !filter && headerTimeVisible
        this.searchVisible = !filter && searchVisible
        this.searchTextVisible = filter
402
403
404
405
        restoreHeaderButtonVisibilities()
    }

    private fun restoreHeaderButtonVisibilities() {
406
        binding.progressBar.visibility = if (progressBarVisible) View.VISIBLE else View.GONE
407
408
        val cl = binding.header
        TransitionManager.beginDelayedTransition(cl, AutoTransition().setDuration(200))
409
        ConstraintSet().apply {
410
            clone(cl)
411
412
413
414
415
416
417
            setVisibility(R.id.playlist_search, if (searchVisible) ConstraintSet.VISIBLE else ConstraintSet.GONE)
            setVisibility(R.id.playlist_switch, if (playlistSwitchVisible) ConstraintSet.VISIBLE else ConstraintSet.GONE)
            setVisibility(R.id.adv_function, if (advFuncVisible) ConstraintSet.VISIBLE else ConstraintSet.GONE)
            setVisibility(R.id.header_play_pause, if (headerPlayPauseVisible) ConstraintSet.VISIBLE else ConstraintSet.GONE)
            setVisibility(R.id.header_time, if (headerTimeVisible) ConstraintSet.VISIBLE else ConstraintSet.GONE)
            setVisibility(R.id.playlist_search_text, if (searchTextVisible) ConstraintSet.VISIBLE else ConstraintSet.GONE)
            setVisibility(R.id.audio_media_switcher, if (searchTextVisible) ConstraintSet.GONE else ConstraintSet.VISIBLE)
418
419
            applyTo(cl)
        }
420
421
    }

Geoffrey Métais's avatar
Geoffrey Métais committed
422
    fun onABRepeat(v: View) {
Geoffrey Métais's avatar
Geoffrey Métais committed
423
        playlistModel.toggleABRepeat()
Geoffrey Métais's avatar
Geoffrey Métais committed
424
425
    }

426
    fun onSearchClick(v: View) {
427
        setHeaderVisibilities(false, false, false, false, false, false, true)
Geoffrey Métais's avatar
Geoffrey Métais committed
428
        binding.playlistSearchText.editText?.requestFocus()
429
        if (binding.showCover) onPlaylistSwitchClick(binding.playlistSwitch)
430
        val imm = v.context.applicationContext.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
431
        imm.showSoftInput(binding.playlistSearchText.editText, InputMethodManager.SHOW_IMPLICIT)
432
        handler.postDelayed(hideSearchRunnable, SEARCH_TIMEOUT_MILLIS)
433
434
435
436
    }

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

437
    fun backPressed(): Boolean {
438
439
440
441
442
443
444
445
        if (this::optionsDelegate.isInitialized && optionsDelegate.isShowing()) {
            optionsDelegate.hide()
            return true
        }
        return clearSearch()
    }

    private fun clearSearch(): Boolean {
446
        if (this::playlistModel.isInitialized) playlistModel.filter(null)
447
448
449
450
        return hideSearchField()
    }

    private fun hideSearchField(): Boolean {
451
452
        if (binding.playlistSearchText.visibility != View.VISIBLE) return false
        binding.playlistSearchText.editText?.apply {
453
454
455
456
            removeTextChangedListener(this@AudioPlayer)
            setText("")
            addTextChangedListener(this@AudioPlayer)
        }
457
        UiTools.setKeyboardVisibility(binding.playlistSearchText, false)
458
459
        if (playerState == com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED) setHeaderVisibilities(false, false, true, true, true, false)
        else setHeaderVisibilities(true, true, false, false, false, true)
460
461
462
463
464
465
        return true
    }

    override fun onTextChanged(charSequence: CharSequence, start: Int, before: Int, count: Int) {
        val length = charSequence.length
        if (length > 1) {
466
467
            playlistModel.filter(charSequence)
            handler.removeCallbacks(hideSearchRunnable)
468
        } else if (length == 0) {
469
            playlistModel.filter(null)
470
471
472
473
474
475
            hideSearchField()
        }
    }

    override fun afterTextChanged(editable: Editable) {}

476
477
478
479
480
    private val playlistObserver = Observer<MutableList<MediaWrapper>> {
        playlistAdapter.update(it!!)
        updateActor.offer(Unit)
    }

Geoffrey Métais's avatar
Geoffrey Métais committed
481
482
    override fun onDestroy() {
        super.onDestroy()
483
        if (this::optionsDelegate.isInitialized) optionsDelegate.release()
484
        playlistModel.dataset.removeObserver(playlistObserver)
485
486
487
    }

    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
488
        internal var length = -1L
489

Geoffrey Métais's avatar
Geoffrey Métais committed
490
491
        internal var possibleSeek = 0
        internal var vibrated = false
492
493
494
495
496

        @RequiresPermission(Manifest.permission.VIBRATE)
        internal var seekRunnable: Runnable = object : Runnable {
            override fun run() {
                if (!vibrated) {
497
                    (VLCApplication.appContext.getSystemService(Context.VIBRATOR_SERVICE) as android.os.Vibrator)
498
499
500
501
502
503
504
505
506
507
508
                            .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
                }

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

        override fun onTouch(v: View, event: MotionEvent): Boolean {
            when (event.action) {
                MotionEvent.ACTION_DOWN -> {
519
                    (if (forward) binding.next else binding.previous).setImageResource(this.pressed)
Geoffrey Métais's avatar
Geoffrey Métais committed
520
                    possibleSeek = playlistModel.time.toInt()
521
                    previewingSeek = true
522
                    vibrated = false
Geoffrey Métais's avatar
Geoffrey Métais committed
523
                    length = playlistModel.length
524
                    handler.postDelayed(seekRunnable, 1000)
525
526
527
528
                    return true
                }

                MotionEvent.ACTION_UP -> {
529
530
531
                    (if (forward) binding.next else binding.previous).setImageResource(this.normal)
                    handler.removeCallbacks(seekRunnable)
                    previewingSeek = false
532
533
534
535
                    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
536
537
                            if (possibleSeek < playlistModel.length)
                                playlistModel.time = possibleSeek.toLong()
538
539
540
541
                            else
                                onNextClick(v)
                        } else {
                            if (possibleSeek > 0)
Geoffrey Métais's avatar
Geoffrey Métais committed
542
                                playlistModel.time = possibleSeek.toLong()
543
544
545
546
547
548
549
550
551
552
553
554
555
                            else
                                onPreviousClick(v)
                        }
                    }
                    return true
                }
            }
            return false
        }
    }

    private fun showPlaylistTips() {
        val activity = activity as? AudioPlayerContainerActivity
Habib Kazemi's avatar
Habib Kazemi committed
556
        activity?.showTipViewIfNeeded(R.id.audio_playlist_tips, PREF_PLAYLIST_TIPS_SHOWN)
557
558
559
    }

    fun onStateChanged(newState: Int) {
560
        playerState = newState
561
        when (newState) {
562
563
            BottomSheetBehavior.STATE_COLLAPSED -> {
                backPressed()
564
                binding.header.setBackgroundResource(DEFAULT_BACKGROUND_DARKER_ID)
Nicolas Pomepuy's avatar
Nicolas Pomepuy committed
565
                setHeaderVisibilities(advFuncVisible = false, playlistSwitchVisible = false, headerPlayPauseVisible = true, progressBarVisible = true, headerTimeVisible = true, searchVisible = false)
566
            }
567
            BottomSheetBehavior.STATE_EXPANDED -> {
568
                binding.header.setBackgroundResource(0)
Nicolas Pomepuy's avatar
Nicolas Pomepuy committed
569
                setHeaderVisibilities(advFuncVisible = true, playlistSwitchVisible = true, headerPlayPauseVisible = false, progressBarVisible = false, headerTimeVisible = false, searchVisible = true)
570
                showPlaylistTips()
Geoffrey Métais's avatar
Geoffrey Métais committed
571
                playlistAdapter.currentIndex = playlistModel.currentMediaPosition
572
            }
573
//            else -> binding.header.setBackgroundResource(0)
574
575
576
        }
    }

Geoffrey Métais's avatar
Geoffrey Métais committed
577
    private var timelineListener: OnSeekBarChangeListener = object : OnSeekBarChangeListener {
578
579
580
581
582
583

        override fun onStopTrackingTouch(seekBar: SeekBar) {}

        override fun onStartTrackingTouch(seekBar: SeekBar) {}

        override fun onProgressChanged(sb: SeekBar, progress: Int, fromUser: Boolean) {
584
            if (fromUser) {
Geoffrey Métais's avatar
Geoffrey Métais committed
585
586
                playlistModel.time = progress.toLong()
                binding.time.text = Tools.millisToString(if (showRemainingTime) progress - playlistModel.length else progress.toLong())
587
                binding.headerTime.text = Tools.millisToString(progress.toLong())
588
589
590
591
            }
        }
    }

Geoffrey Métais's avatar
Geoffrey Métais committed
592
    private val headerMediaSwitcherListener = object : AudioMediaSwitcherListener {
593
594
595
596

        override fun onMediaSwitching() {}

        override fun onMediaSwitched(position: Int) {
597
598
599
600
            when (position) {
                AudioMediaSwitcherListener.PREVIOUS_MEDIA -> playlistModel.previous(true)
                AudioMediaSwitcherListener.NEXT_MEDIA -> playlistModel.next()
            }
601
602
603
604
605
606
        }

        override fun onTouchClick() {
            val activity = activity as AudioPlayerContainerActivity
            activity.slideUpOrDownAudioPlayer()
        }
607
608
609
610

        override fun onTouchDown() {}

        override fun onTouchUp() {}
611
612
613
614
    }

    private val mCoverMediaSwitcherListener = object : AudioMediaSwitcherListener {

615
        override fun onMediaSwitching() {
616
            (activity as? AudioPlayerContainerActivity)?.bottomSheetBehavior?.lock(true)
617
        }
618
619

        override fun onMediaSwitched(position: Int) {
Geoffrey Métais's avatar
Geoffrey Métais committed
620
621
622
            when (position) {
                AudioMediaSwitcherListener.PREVIOUS_MEDIA -> playlistModel.previous(true)
                AudioMediaSwitcherListener.NEXT_MEDIA -> playlistModel.next()
623
            }
624
            (activity as? AudioPlayerContainerActivity)?.bottomSheetBehavior?.lock(false)
625
626
627
628
629
630
631
632
633
        }

        override fun onTouchDown() {}

        override fun onTouchUp() {}

        override fun onTouchClick() {}
    }

Geoffrey Métais's avatar
Geoffrey Métais committed
634
635
636
    private val hideSearchRunnable by lazy(LazyThreadSafetyMode.NONE) {
        Runnable {
            hideSearchField()
637
            playlistModel.filter(null)
Geoffrey Métais's avatar
Geoffrey Métais committed
638
        }
639
640
    }
}