summaryrefslogtreecommitdiff
path: root/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt
blob: bbea140ecfaf8d8650b10bc5608aac19dfb22ef7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.media

import android.media.session.MediaController
import android.media.session.PlaybackState
import android.os.SystemProperties
import android.util.Log
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
import com.android.systemui.util.concurrency.DelayableExecutor
import java.util.concurrent.TimeUnit
import javax.inject.Inject

private const val DEBUG = true
private const val TAG = "MediaTimeout"
private val PAUSED_MEDIA_TIMEOUT = SystemProperties
        .getLong("debug.sysui.media_timeout", TimeUnit.MINUTES.toMillis(10))

/**
 * Controller responsible for keeping track of playback states and expiring inactive streams.
 */
@SysUISingleton
class MediaTimeoutListener @Inject constructor(
    private val mediaControllerFactory: MediaControllerFactory,
    @Main private val mainExecutor: DelayableExecutor
) : MediaDataManager.Listener {

    private val mediaListeners: MutableMap<String, PlaybackStateListener> = mutableMapOf()

    /**
     * Callback representing that a media object is now expired:
     * @param token Media session unique identifier
     * @param pauseTimeout True when expired for {@code PAUSED_MEDIA_TIMEOUT}
     */
    lateinit var timeoutCallback: (String, Boolean) -> Unit

    override fun onMediaDataLoaded(
        key: String,
        oldKey: String?,
        data: MediaData,
        immediately: Boolean
    ) {
        var reusedListener: PlaybackStateListener? = null

        // First check if we already have a listener
        mediaListeners.get(key)?.let {
            if (!it.destroyed) {
                return
            }

            // If listener was destroyed previously, we'll need to re-register it
            if (DEBUG) {
                Log.d(TAG, "Reusing destroyed listener $key")
            }
            reusedListener = it
        }

        // Having an old key means that we're migrating from/to resumption. We should update
        // the old listener to make sure that events will be dispatched to the new location.
        val migrating = oldKey != null && key != oldKey
        if (migrating) {
            reusedListener = mediaListeners.remove(oldKey)
            if (reusedListener != null) {
                if (DEBUG) Log.d(TAG, "migrating key $oldKey to $key, for resumption")
            } else {
                Log.w(TAG, "Old key $oldKey for player $key doesn't exist. Continuing...")
            }
        }

        reusedListener?.let {
            val wasPlaying = it.playing ?: false
            if (DEBUG) Log.d(TAG, "updating listener for $key, was playing? $wasPlaying")
            it.mediaData = data
            it.key = key
            mediaListeners[key] = it
            if (wasPlaying != it.playing) {
                // If a player becomes active because of a migration, we'll need to broadcast
                // its state. Doing it now would lead to reentrant callbacks, so let's wait
                // until we're done.
                mainExecutor.execute {
                    if (mediaListeners[key]?.playing == true) {
                        if (DEBUG) Log.d(TAG, "deliver delayed playback state for $key")
                        timeoutCallback.invoke(key, false /* timedOut */)
                    }
                }
            }
            return
        }

        mediaListeners[key] = PlaybackStateListener(key, data)
    }

    override fun onMediaDataRemoved(key: String) {
        mediaListeners.remove(key)?.destroy()
    }

    fun isTimedOut(key: String): Boolean {
        return mediaListeners[key]?.timedOut ?: false
    }

    private inner class PlaybackStateListener(
        var key: String,
        data: MediaData
    ) : MediaController.Callback() {

        var timedOut = false
        var playing: Boolean? = null
        var destroyed = false

        var mediaData: MediaData = data
            set(value) {
                destroyed = false
                mediaController?.unregisterCallback(this)
                field = value
                mediaController = if (field.token != null) {
                    mediaControllerFactory.create(field.token)
                } else {
                    null
                }
                mediaController?.registerCallback(this)
                // Let's register the cancellations, but not dispatch events now.
                // Timeouts didn't happen yet and reentrant events are troublesome.
                processState(mediaController?.playbackState, dispatchEvents = false)
            }

        // Resume controls may have null token
        private var mediaController: MediaController? = null
        private var cancellation: Runnable? = null

        init {
            mediaData = data
        }

        fun destroy() {
            mediaController?.unregisterCallback(this)
            cancellation?.run()
            destroyed = true
        }

        override fun onPlaybackStateChanged(state: PlaybackState?) {
            processState(state, dispatchEvents = true)
        }

        override fun onSessionDestroyed() {
            // If the session is destroyed, the controller is no longer valid, and we will need to
            // recreate it if this key is updated later
            if (DEBUG) {
                Log.d(TAG, "Session destroyed for $key")
            }
            destroy()
        }

        private fun processState(state: PlaybackState?, dispatchEvents: Boolean) {
            if (DEBUG) {
                Log.v(TAG, "processState $key: $state")
            }

            val isPlaying = state != null && isPlayingState(state.state)
            if (playing == isPlaying && playing != null) {
                return
            }
            playing = isPlaying

            if (!isPlaying) {
                if (DEBUG) {
                    Log.v(TAG, "schedule timeout for $key")
                }
                if (cancellation != null) {
                    if (DEBUG) Log.d(TAG, "cancellation already exists, continuing.")
                    return
                }
                expireMediaTimeout(key, "PLAYBACK STATE CHANGED - $state")
                cancellation = mainExecutor.executeDelayed({
                    cancellation = null
                    if (DEBUG) {
                        Log.v(TAG, "Execute timeout for $key")
                    }
                    timedOut = true
                    // this event is async, so it's safe even when `dispatchEvents` is false
                    timeoutCallback(key, timedOut)
                }, PAUSED_MEDIA_TIMEOUT)
            } else {
                expireMediaTimeout(key, "playback started - $state, $key")
                timedOut = false
                if (dispatchEvents) {
                    timeoutCallback(key, timedOut)
                }
            }
        }

        private fun expireMediaTimeout(mediaKey: String, reason: String) {
            cancellation?.apply {
                if (DEBUG) {
                    Log.v(TAG, "media timeout cancelled for  $mediaKey, reason: $reason")
                }
                run()
            }
            cancellation = null
        }
    }
}