/* * 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.controls.ui.controller import com.android.app.tracing.traceSection import com.android.systemui.Dumpable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager import com.android.systemui.media.controls.ui.view.MediaHostState import com.android.systemui.util.animation.MeasurementOutput import java.io.PrintWriter import javax.inject.Inject private val TAG = "MediaHostStatesManager" /** * A class responsible for managing all media host states of the various host locations and * coordinating the heights among different players. This class can be used to get the most up to * date state for any location. */ @SysUISingleton class MediaHostStatesManager @Inject constructor(dumpManager: DumpManager) : Dumpable { private val callbacks: MutableSet = mutableSetOf() private val controllers: MutableSet = mutableSetOf() /** * The overall sizes of the carousel. This is needed to make sure all players in the carousel * have equal size. */ val carouselSizes: MutableMap = mutableMapOf() /** A map with all media states of all locations. */ val mediaHostStates: MutableMap = mutableMapOf() init { dumpManager.registerNormalDumpable(TAG, this) } /** * Notify that a media state for a given location has changed. Should only be called from Media * hosts themselves. */ fun updateHostState(@MediaLocation location: Int, hostState: MediaHostState) = traceSection("MediaHostStatesManager#updateHostState") { val currentState = mediaHostStates.get(location) if (!hostState.equals(currentState)) { val newState = hostState.copy() mediaHostStates.put(location, newState) updateCarouselDimensions(location, hostState) // First update all the controllers to ensure they get the chance to measure for (controller in controllers) { controller.stateCallback.onHostStateChanged(location, newState) } // Then update all other callbacks which may depend on the controllers above for (callback in callbacks) { callback.onHostStateChanged(location, newState) } } } /** * Get the dimensions of all players combined, which determines the overall height of the media * carousel and the media hosts. */ fun updateCarouselDimensions( @MediaLocation location: Int, hostState: MediaHostState, ): MeasurementOutput = traceSection("MediaHostStatesManager#updateCarouselDimensions") { val result = MeasurementOutput(0, 0) var changed = false for (controller in controllers) { val measurement = controller.getMeasurementsForState(hostState) measurement?.let { if (it.measuredHeight > result.measuredHeight) { result.measuredHeight = it.measuredHeight changed = true } if (it.measuredWidth > result.measuredWidth) { result.measuredWidth = it.measuredWidth changed = true } } } // Set carousel size if result measurements changed. This avoids setting carousel // size when this method gets called before the addition of media view controllers if (!carouselSizes.contains(location) || changed) { carouselSizes[location] = result } return carouselSizes[location] ?: result } /** Add a callback to be called when a MediaState has updated */ fun addCallback(callback: Callback) { callbacks.add(callback) } /** Remove a callback that listens to media states */ fun removeCallback(callback: Callback) { callbacks.remove(callback) } /** * Register a controller that listens to media states and is used to determine the size of the * media carousel */ fun addController(controller: MediaViewController) { controllers.add(controller) } /** Notify the manager about the removal of a controller. */ fun removeController(controller: MediaViewController) { controllers.remove(controller) } override fun dump(pw: PrintWriter, args: Array) { pw.apply { println("Controllers: $controllers") println("Callbacks: $callbacks") for ((location, size) in carouselSizes) { println("Size $location: ${size.measuredWidth} x ${size.measuredHeight}") } for ((location, state) in mediaHostStates) { println("Host $location: visible ${state.visible}") } } } interface Callback { /** * Notify the callbacks that a media state for a host has changed, and that the * corresponding view states should be updated and applied */ fun onHostStateChanged(@MediaLocation location: Int, mediaHostState: MediaHostState) } }