blob: dd83e42cde2dbc1b8864a37a0ff530bcc54cfef1 [file] [log] [blame]
Robert Snoeberger7dffd372020-04-01 17:32:44 -04001/*
2 * Copyright (C) 2020 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.systemui.media
18
19import android.media.MediaMetadata
20import android.media.session.MediaController
21import android.media.session.PlaybackState
22import android.view.MotionEvent
23import android.view.View
24import android.widget.SeekBar
25import androidx.annotation.AnyThread
26import androidx.annotation.WorkerThread
27import androidx.lifecycle.MutableLiveData
28import androidx.lifecycle.LiveData
29
30import com.android.systemui.util.concurrency.DelayableExecutor
31
32private const val POSITION_UPDATE_INTERVAL_MILLIS = 100L
33
34/** ViewModel for seek bar in QS media player. */
35class SeekBarViewModel(val bgExecutor: DelayableExecutor) {
36
Robert Snoeberger0cf2bc22020-04-10 18:47:47 -040037 private var _data = Progress(false, false, null, null, null)
38 set(value) {
39 field = value
40 _progress.postValue(value)
41 }
Robert Snoeberger7dffd372020-04-01 17:32:44 -040042 private val _progress = MutableLiveData<Progress>().apply {
Robert Snoeberger0cf2bc22020-04-10 18:47:47 -040043 postValue(_data)
Robert Snoeberger7dffd372020-04-01 17:32:44 -040044 }
45 val progress: LiveData<Progress>
46 get() = _progress
47 private var controller: MediaController? = null
48 private var playbackState: PlaybackState? = null
49
50 /** Listening state (QS open or closed) is used to control polling of progress. */
51 var listening = true
52 set(value) {
53 if (value) {
54 checkPlaybackPosition()
55 }
56 }
57
58 /**
59 * Handle request to change the current position in the media track.
60 * @param position Place to seek to in the track.
61 */
62 @WorkerThread
63 fun onSeek(position: Long) {
64 controller?.transportControls?.seekTo(position)
65 }
66
67 /**
68 * Updates media information.
69 * @param mediaController controller for media session
70 * @param color foreground color for UI elements
71 */
72 @WorkerThread
73 fun updateController(mediaController: MediaController?, color: Int) {
74 controller = mediaController
75 playbackState = controller?.playbackState
76 val mediaMetadata = controller?.metadata
77 val seekAvailable = ((playbackState?.actions ?: 0L) and PlaybackState.ACTION_SEEK_TO) != 0L
78 val position = playbackState?.position?.toInt()
79 val duration = mediaMetadata?.getLong(MediaMetadata.METADATA_KEY_DURATION)?.toInt()
80 val enabled = if (duration != null && duration <= 0) false else true
Robert Snoeberger0cf2bc22020-04-10 18:47:47 -040081 _data = Progress(enabled, seekAvailable, position, duration, color)
Robert Snoeberger7dffd372020-04-01 17:32:44 -040082 if (shouldPollPlaybackPosition()) {
83 checkPlaybackPosition()
84 }
85 }
86
87 @AnyThread
88 private fun checkPlaybackPosition(): Runnable = bgExecutor.executeDelayed({
89 val currentPosition = controller?.playbackState?.position?.toInt()
Robert Snoeberger0cf2bc22020-04-10 18:47:47 -040090 if (currentPosition != null && _data.elapsedTime != currentPosition) {
91 _data = _data.copy(elapsedTime = currentPosition)
Robert Snoeberger7dffd372020-04-01 17:32:44 -040092 }
93 if (shouldPollPlaybackPosition()) {
94 checkPlaybackPosition()
95 }
96 }, POSITION_UPDATE_INTERVAL_MILLIS)
97
98 @WorkerThread
99 private fun shouldPollPlaybackPosition(): Boolean {
100 val state = playbackState?.state
101 val moving = if (state == null) false else
102 state == PlaybackState.STATE_PLAYING ||
103 state == PlaybackState.STATE_BUFFERING ||
104 state == PlaybackState.STATE_FAST_FORWARDING ||
105 state == PlaybackState.STATE_REWINDING
106 return moving && listening
107 }
108
109 /** Gets a listener to attach to the seek bar to handle seeking. */
110 val seekBarListener: SeekBar.OnSeekBarChangeListener
111 get() {
112 return SeekBarChangeListener(this, bgExecutor)
113 }
114
115 /** Gets a listener to attach to the seek bar to disable touch intercepting. */
116 val seekBarTouchListener: View.OnTouchListener
117 get() {
118 return SeekBarTouchListener()
119 }
120
121 private class SeekBarChangeListener(
122 val viewModel: SeekBarViewModel,
123 val bgExecutor: DelayableExecutor
124 ) : SeekBar.OnSeekBarChangeListener {
125 override fun onProgressChanged(bar: SeekBar, progress: Int, fromUser: Boolean) {
126 if (fromUser) {
127 bgExecutor.execute {
128 viewModel.onSeek(progress.toLong())
129 }
130 }
131 }
132 override fun onStartTrackingTouch(bar: SeekBar) {
133 }
134 override fun onStopTrackingTouch(bar: SeekBar) {
135 val pos = bar.progress.toLong()
136 bgExecutor.execute {
137 viewModel.onSeek(pos)
138 }
139 }
140 }
141
142 private class SeekBarTouchListener : View.OnTouchListener {
143 override fun onTouch(view: View, event: MotionEvent): Boolean {
144 view.parent.requestDisallowInterceptTouchEvent(true)
145 return view.onTouchEvent(event)
146 }
147 }
148
149 /** State seen by seek bar UI. */
150 data class Progress(
151 val enabled: Boolean,
152 val seekAvailable: Boolean,
153 val elapsedTime: Int?,
154 val duration: Int?,
155 val color: Int?
156 )
157}