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