blob: 9a134dbe02647ae78e0356319832ea002d9baa54 [file] [log] [blame]
Lucas Dupin6f0bd312020-05-28 18:19:29 -07001/*
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.session.MediaController
20import android.media.session.PlaybackState
21import android.os.SystemProperties
22import android.util.Log
23import com.android.systemui.dagger.qualifiers.Main
24import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
25import com.android.systemui.util.concurrency.DelayableExecutor
26import java.util.concurrent.TimeUnit
27import javax.inject.Inject
28import javax.inject.Singleton
29
30private const val DEBUG = true
31private const val TAG = "MediaTimeout"
32private 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@Singleton
39class 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
Lucas Dupin84f5a0e2020-06-08 19:55:33 -070046 /**
47 * Callback representing that a media object is now expired:
48 * @param token Media session unique identifier
49 * @param pauseTimeuot True when expired for {@code PAUSED_MEDIA_TIMEOUT}
50 */
Lucas Dupin6f0bd312020-05-28 18:19:29 -070051 lateinit var timeoutCallback: (String, Boolean) -> Unit
52
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -040053 override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
Lucas Dupin6f0bd312020-05-28 18:19:29 -070054 if (mediaListeners.containsKey(key)) {
55 return
56 }
57 mediaListeners[key] = PlaybackStateListener(key, data)
58 }
59
60 override fun onMediaDataRemoved(key: String) {
61 mediaListeners.remove(key)?.destroy()
62 }
63
64 fun isTimedOut(key: String): Boolean {
65 return mediaListeners[key]?.timedOut ?: false
66 }
67
68 private inner class PlaybackStateListener(
69 private val key: String,
70 data: MediaData
71 ) : MediaController.Callback() {
72
73 var timedOut = false
Lucas Dupine267f4d2020-06-22 22:55:23 -070074 private var playing: Boolean? = null
Lucas Dupin6f0bd312020-05-28 18:19:29 -070075
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -040076 // Resume controls may have null token
77 private val mediaController = if (data.token != null) {
78 mediaControllerFactory.create(data.token)
79 } else {
80 null
81 }
Lucas Dupin6f0bd312020-05-28 18:19:29 -070082 private var cancellation: Runnable? = null
83
84 init {
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -040085 mediaController?.registerCallback(this)
Lucas Dupin281d6462020-06-16 16:51:03 -070086 onPlaybackStateChanged(mediaController?.playbackState)
Lucas Dupin6f0bd312020-05-28 18:19:29 -070087 }
88
89 fun destroy() {
Beth Thibodeauf55bc6a2020-05-20 02:01:31 -040090 mediaController?.unregisterCallback(this)
Lucas Dupin6f0bd312020-05-28 18:19:29 -070091 }
92
93 override fun onPlaybackStateChanged(state: PlaybackState?) {
94 if (DEBUG) {
95 Log.v(TAG, "onPlaybackStateChanged: $state")
96 }
Lucas Dupin6f0bd312020-05-28 18:19:29 -070097
Lucas Dupine267f4d2020-06-22 22:55:23 -070098 val isPlaying = state != null && isPlayingState(state.state)
99 if (playing == isPlaying && playing != null) {
100 return
101 }
102 playing = isPlaying
103
104 if (!isPlaying) {
Lucas Dupin6f0bd312020-05-28 18:19:29 -0700105 if (DEBUG) {
106 Log.v(TAG, "schedule timeout for $key")
107 }
Lucas Dupin6c4a1d02020-06-04 18:22:36 -0700108 if (cancellation != null) {
109 if (DEBUG) Log.d(TAG, "cancellation already exists, continuing.")
110 return
111 }
Lucas Dupin6f0bd312020-05-28 18:19:29 -0700112 expireMediaTimeout(key, "PLAYBACK STATE CHANGED - $state")
113 cancellation = mainExecutor.executeDelayed({
114 cancellation = null
115 if (DEBUG) {
116 Log.v(TAG, "Execute timeout for $key")
117 }
118 timedOut = true
119 timeoutCallback(key, timedOut)
120 }, PAUSED_MEDIA_TIMEOUT)
121 } else {
Lucas Dupin6c4a1d02020-06-04 18:22:36 -0700122 expireMediaTimeout(key, "playback started - $state, $key")
Lucas Dupin6f0bd312020-05-28 18:19:29 -0700123 timedOut = false
124 timeoutCallback(key, timedOut)
125 }
126 }
127
Lucas Dupin84f5a0e2020-06-08 19:55:33 -0700128 private fun expireMediaTimeout(mediaKey: String, reason: String) {
Lucas Dupin6f0bd312020-05-28 18:19:29 -0700129 cancellation?.apply {
130 if (DEBUG) {
131 Log.v(TAG,
Lucas Dupin84f5a0e2020-06-08 19:55:33 -0700132 "media timeout cancelled for $mediaKey, reason: $reason")
Lucas Dupin6f0bd312020-05-28 18:19:29 -0700133 }
134 run()
135 }
136 cancellation = null
137 }
138 }
139}