• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
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 
17 package com.android.systemui.media
18 
19 import android.media.session.MediaController
20 import android.media.session.PlaybackState
21 import android.os.SystemProperties
22 import android.util.Log
23 import com.android.systemui.dagger.SysUISingleton
24 import com.android.systemui.dagger.qualifiers.Main
25 import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
26 import com.android.systemui.util.concurrency.DelayableExecutor
27 import java.util.concurrent.TimeUnit
28 import javax.inject.Inject
29 
30 private const val DEBUG = true
31 private const val TAG = "MediaTimeout"
32 private val PAUSED_MEDIA_TIMEOUT = SystemProperties
33         .getLong("debug.sysui.media_timeout", TimeUnit.MINUTES.toMillis(10))
34 
35 /**
36  * Controller responsible for keeping track of playback states and expiring inactive streams.
37  */
38 @SysUISingleton
39 class MediaTimeoutListener @Inject constructor(
40     private val mediaControllerFactory: MediaControllerFactory,
41     @Main private val mainExecutor: DelayableExecutor
42 ) : MediaDataManager.Listener {
43 
44     private val mediaListeners: MutableMap<String, PlaybackStateListener> = mutableMapOf()
45 
46     /**
47      * Callback representing that a media object is now expired:
48      * @param token Media session unique identifier
49      * @param pauseTimeout True when expired for {@code PAUSED_MEDIA_TIMEOUT}
50      */
51     lateinit var timeoutCallback: (String, Boolean) -> Unit
52 
onMediaDataLoadednull53     override fun onMediaDataLoaded(
54         key: String,
55         oldKey: String?,
56         data: MediaData,
57         immediately: Boolean,
58         isSsReactivated: Boolean
59     ) {
60         var reusedListener: PlaybackStateListener? = null
61 
62         // First check if we already have a listener
63         mediaListeners.get(key)?.let {
64             if (!it.destroyed) {
65                 return
66             }
67 
68             // If listener was destroyed previously, we'll need to re-register it
69             if (DEBUG) {
70                 Log.d(TAG, "Reusing destroyed listener $key")
71             }
72             reusedListener = it
73         }
74 
75         // Having an old key means that we're migrating from/to resumption. We should update
76         // the old listener to make sure that events will be dispatched to the new location.
77         val migrating = oldKey != null && key != oldKey
78         if (migrating) {
79             reusedListener = mediaListeners.remove(oldKey)
80             if (reusedListener != null) {
81                 if (DEBUG) Log.d(TAG, "migrating key $oldKey to $key, for resumption")
82             } else {
83                 Log.w(TAG, "Old key $oldKey for player $key doesn't exist. Continuing...")
84             }
85         }
86 
87         reusedListener?.let {
88             val wasPlaying = it.playing ?: false
89             if (DEBUG) Log.d(TAG, "updating listener for $key, was playing? $wasPlaying")
90             it.mediaData = data
91             it.key = key
92             mediaListeners[key] = it
93             if (wasPlaying != it.playing) {
94                 // If a player becomes active because of a migration, we'll need to broadcast
95                 // its state. Doing it now would lead to reentrant callbacks, so let's wait
96                 // until we're done.
97                 mainExecutor.execute {
98                     if (mediaListeners[key]?.playing == true) {
99                         if (DEBUG) Log.d(TAG, "deliver delayed playback state for $key")
100                         timeoutCallback.invoke(key, false /* timedOut */)
101                     }
102                 }
103             }
104             return
105         }
106 
107         mediaListeners[key] = PlaybackStateListener(key, data)
108     }
109 
onMediaDataRemovednull110     override fun onMediaDataRemoved(key: String) {
111         mediaListeners.remove(key)?.destroy()
112     }
113 
isTimedOutnull114     fun isTimedOut(key: String): Boolean {
115         return mediaListeners[key]?.timedOut ?: false
116     }
117 
118     private inner class PlaybackStateListener(
119         var key: String,
120         data: MediaData
121     ) : MediaController.Callback() {
122 
123         var timedOut = false
124         var playing: Boolean? = null
125         var destroyed = false
126 
127         var mediaData: MediaData = data
128             set(value) {
129                 destroyed = false
130                 mediaController?.unregisterCallback(this)
131                 field = value
132                 mediaController = if (field.token != null) {
133                     mediaControllerFactory.create(field.token)
134                 } else {
135                     null
136                 }
137                 mediaController?.registerCallback(this)
138                 // Let's register the cancellations, but not dispatch events now.
139                 // Timeouts didn't happen yet and reentrant events are troublesome.
140                 processState(mediaController?.playbackState, dispatchEvents = false)
141             }
142 
143         // Resume controls may have null token
144         private var mediaController: MediaController? = null
145         private var cancellation: Runnable? = null
146 
<lambda>null147         init {
148             mediaData = data
149         }
150 
destroynull151         fun destroy() {
152             mediaController?.unregisterCallback(this)
153             cancellation?.run()
154             destroyed = true
155         }
156 
onPlaybackStateChangednull157         override fun onPlaybackStateChanged(state: PlaybackState?) {
158             processState(state, dispatchEvents = true)
159         }
160 
onSessionDestroyednull161         override fun onSessionDestroyed() {
162             // If the session is destroyed, the controller is no longer valid, and we will need to
163             // recreate it if this key is updated later
164             if (DEBUG) {
165                 Log.d(TAG, "Session destroyed for $key")
166             }
167             destroy()
168         }
169 
processStatenull170         private fun processState(state: PlaybackState?, dispatchEvents: Boolean) {
171             if (DEBUG) {
172                 Log.v(TAG, "processState $key: $state")
173             }
174 
175             val isPlaying = state != null && isPlayingState(state.state)
176             if (playing == isPlaying && playing != null) {
177                 return
178             }
179             playing = isPlaying
180 
181             if (!isPlaying) {
182                 if (DEBUG) {
183                     Log.v(TAG, "schedule timeout for $key")
184                 }
185                 if (cancellation != null) {
186                     if (DEBUG) Log.d(TAG, "cancellation already exists, continuing.")
187                     return
188                 }
189                 expireMediaTimeout(key, "PLAYBACK STATE CHANGED - $state")
190                 cancellation = mainExecutor.executeDelayed({
191                     cancellation = null
192                     if (DEBUG) {
193                         Log.v(TAG, "Execute timeout for $key")
194                     }
195                     timedOut = true
196                     // this event is async, so it's safe even when `dispatchEvents` is false
197                     timeoutCallback(key, timedOut)
198                 }, PAUSED_MEDIA_TIMEOUT)
199             } else {
200                 expireMediaTimeout(key, "playback started - $state, $key")
201                 timedOut = false
202                 if (dispatchEvents) {
203                     timeoutCallback(key, timedOut)
204                 }
205             }
206         }
207 
expireMediaTimeoutnull208         private fun expireMediaTimeout(mediaKey: String, reason: String) {
209             cancellation?.apply {
210                 if (DEBUG) {
211                     Log.v(TAG, "media timeout cancelled for  $mediaKey, reason: $reason")
212                 }
213                 run()
214             }
215             cancellation = null
216         }
217     }
218 }
219