blob: d6f0de83ecc14381b66ea4765c28ec8448dcfb3f [file] [log] [blame]
package com.android.systemui.shade
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.view.WindowInsets
import androidx.annotation.VisibleForTesting
import androidx.constraintlayout.widget.ConstraintSet
import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
import androidx.constraintlayout.widget.ConstraintSet.END
import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
import androidx.constraintlayout.widget.ConstraintSet.START
import androidx.constraintlayout.widget.ConstraintSet.TOP
import com.android.systemui.R
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.navigationbar.NavigationModeController
import com.android.systemui.plugins.qs.QS
import com.android.systemui.plugins.qs.QSContainerController
import com.android.systemui.recents.OverviewProxyService
import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener
import com.android.systemui.shared.system.QuickStepContract
import com.android.systemui.util.LargeScreenUtils
import com.android.systemui.util.ViewController
import com.android.systemui.util.concurrency.DelayableExecutor
import java.util.function.Consumer
import javax.inject.Inject
import kotlin.reflect.KMutableProperty0
@VisibleForTesting
internal const val INSET_DEBOUNCE_MILLIS = 500L
class NotificationsQSContainerController @Inject constructor(
view: NotificationsQuickSettingsContainer,
private val navigationModeController: NavigationModeController,
private val overviewProxyService: OverviewProxyService,
private val largeScreenShadeHeaderController: LargeScreenShadeHeaderController,
private val featureFlags: FeatureFlags,
@Main private val delayableExecutor: DelayableExecutor
) : ViewController<NotificationsQuickSettingsContainer>(view), QSContainerController {
var qsExpanded = false
set(value) {
if (field != value) {
field = value
mView.invalidate()
}
}
private var splitShadeEnabled = false
private var isQSDetailShowing = false
private var isQSCustomizing = false
private var isQSCustomizerAnimating = false
private var largeScreenShadeHeaderHeight = 0
private var largeScreenShadeHeaderActive = false
private var notificationsBottomMargin = 0
private var scrimShadeBottomMargin = 0
private var footerActionsOffset = 0
private var bottomStableInsets = 0
private var bottomCutoutInsets = 0
private var panelMarginHorizontal = 0
private var topMargin = 0
private val useCombinedQSHeaders = featureFlags.isEnabled(Flags.COMBINED_QS_HEADERS)
private var isGestureNavigation = true
private var taskbarVisible = false
private val taskbarVisibilityListener: OverviewProxyListener = object : OverviewProxyListener {
override fun onTaskbarStatusUpdated(visible: Boolean, stashed: Boolean) {
taskbarVisible = visible
}
}
// With certain configuration changes (like light/dark changes), the nav bar will disappear
// for a bit, causing `bottomStableInsets` to be unstable for some time. Debounce the value
// for 500ms.
// All interactions with this object happen in the main thread.
private val delayedInsetSetter = object : Runnable, Consumer<WindowInsets> {
private var canceller: Runnable? = null
private var stableInsets = 0
private var cutoutInsets = 0
override fun accept(insets: WindowInsets) {
// when taskbar is visible, stableInsetBottom will include its height
stableInsets = insets.stableInsetBottom
cutoutInsets = insets.displayCutout?.safeInsetBottom ?: 0
canceller?.run()
canceller = delayableExecutor.executeDelayed(this, INSET_DEBOUNCE_MILLIS)
}
override fun run() {
bottomStableInsets = stableInsets
bottomCutoutInsets = cutoutInsets
updateBottomSpacing()
}
}
override fun onInit() {
val currentMode: Int = navigationModeController.addListener { mode: Int ->
isGestureNavigation = QuickStepContract.isGesturalMode(mode)
}
isGestureNavigation = QuickStepContract.isGesturalMode(currentMode)
}
public override fun onViewAttached() {
updateResources()
overviewProxyService.addCallback(taskbarVisibilityListener)
mView.setInsetsChangedListener(delayedInsetSetter)
mView.setQSFragmentAttachedListener { qs: QS -> qs.setContainerController(this) }
mView.setConfigurationChangedListener { updateResources() }
}
override fun onViewDetached() {
overviewProxyService.removeCallback(taskbarVisibilityListener)
mView.removeOnInsetsChangedListener()
mView.removeQSFragmentAttachedListener()
mView.setConfigurationChangedListener(null)
}
fun updateResources() {
val newSplitShadeEnabled = LargeScreenUtils.shouldUseSplitNotificationShade(resources)
val splitShadeEnabledChanged = newSplitShadeEnabled != splitShadeEnabled
splitShadeEnabled = newSplitShadeEnabled
largeScreenShadeHeaderActive = LargeScreenUtils.shouldUseLargeScreenShadeHeader(resources)
notificationsBottomMargin = resources.getDimensionPixelSize(
R.dimen.notification_panel_margin_bottom)
largeScreenShadeHeaderHeight =
resources.getDimensionPixelSize(R.dimen.large_screen_shade_header_height)
panelMarginHorizontal = resources.getDimensionPixelSize(
R.dimen.notification_panel_margin_horizontal)
topMargin = if (largeScreenShadeHeaderActive) {
largeScreenShadeHeaderHeight
} else {
resources.getDimensionPixelSize(R.dimen.notification_panel_margin_top)
}
updateConstraints()
val scrimMarginChanged = ::scrimShadeBottomMargin.setAndReportChange(
resources.getDimensionPixelSize(R.dimen.split_shade_notifications_scrim_margin_bottom)
)
val footerOffsetChanged = ::footerActionsOffset.setAndReportChange(
resources.getDimensionPixelSize(R.dimen.qs_footer_action_inset) +
resources.getDimensionPixelSize(R.dimen.qs_footer_actions_bottom_padding)
)
val dimensChanged = scrimMarginChanged || footerOffsetChanged
if (splitShadeEnabledChanged || dimensChanged) {
updateBottomSpacing()
}
}
override fun setCustomizerAnimating(animating: Boolean) {
if (isQSCustomizerAnimating != animating) {
isQSCustomizerAnimating = animating
mView.invalidate()
}
}
override fun setCustomizerShowing(showing: Boolean, animationDuration: Long) {
if (showing != isQSCustomizing) {
isQSCustomizing = showing
largeScreenShadeHeaderController.startCustomizingAnimation(showing, animationDuration)
updateBottomSpacing()
}
}
override fun setDetailShowing(showing: Boolean) {
isQSDetailShowing = showing
updateBottomSpacing()
}
private fun updateBottomSpacing() {
val (containerPadding, notificationsMargin, qsContainerPadding) = calculateBottomSpacing()
mView.setPadding(0, 0, 0, containerPadding)
mView.setNotificationsMarginBottom(notificationsMargin)
mView.setQSContainerPaddingBottom(qsContainerPadding)
}
private fun calculateBottomSpacing(): Paddings {
val containerPadding: Int
val stackScrollMargin: Int
if (!splitShadeEnabled && (isQSCustomizing || isQSDetailShowing)) {
// Clear out bottom paddings/margins so the qs customization can be full height.
containerPadding = 0
stackScrollMargin = 0
} else if (isGestureNavigation) {
// only default cutout padding, taskbar always hides
containerPadding = bottomCutoutInsets
stackScrollMargin = notificationsBottomMargin
} else if (taskbarVisible) {
// navigation buttons + visible taskbar means we're NOT on homescreen
containerPadding = bottomStableInsets
stackScrollMargin = notificationsBottomMargin
} else {
// navigation buttons + hidden taskbar means we're on homescreen
containerPadding = 0
stackScrollMargin = bottomStableInsets + notificationsBottomMargin
}
val qsContainerPadding = if (!(isQSCustomizing || isQSDetailShowing)) {
// We also want this padding in the bottom in these cases
if (splitShadeEnabled) {
stackScrollMargin - scrimShadeBottomMargin - footerActionsOffset
} else {
bottomStableInsets
}
} else {
0
}
return Paddings(containerPadding, stackScrollMargin, qsContainerPadding)
}
fun updateConstraints() {
// To change the constraints at runtime, all children of the ConstraintLayout must have ids
ensureAllViewsHaveIds(mView)
val constraintSet = ConstraintSet()
constraintSet.clone(mView)
setKeyguardStatusViewConstraints(constraintSet)
setQsConstraints(constraintSet)
setNotificationsConstraints(constraintSet)
setLargeScreenShadeHeaderConstraints(constraintSet)
mView.applyConstraints(constraintSet)
}
private fun setLargeScreenShadeHeaderConstraints(constraintSet: ConstraintSet) {
if (largeScreenShadeHeaderActive) {
constraintSet.constrainHeight(R.id.split_shade_status_bar, largeScreenShadeHeaderHeight)
} else {
if (useCombinedQSHeaders) {
constraintSet.constrainHeight(R.id.split_shade_status_bar, WRAP_CONTENT)
}
}
}
private fun setNotificationsConstraints(constraintSet: ConstraintSet) {
val startConstraintId = if (splitShadeEnabled) R.id.qs_edge_guideline else PARENT_ID
constraintSet.apply {
connect(R.id.notification_stack_scroller, START, startConstraintId, START)
setMargin(R.id.notification_stack_scroller, START,
if (splitShadeEnabled) 0 else panelMarginHorizontal)
setMargin(R.id.notification_stack_scroller, END, panelMarginHorizontal)
setMargin(R.id.notification_stack_scroller, TOP, topMargin)
setMargin(R.id.notification_stack_scroller, BOTTOM, notificationsBottomMargin)
}
}
private fun setQsConstraints(constraintSet: ConstraintSet) {
val endConstraintId = if (splitShadeEnabled) R.id.qs_edge_guideline else PARENT_ID
constraintSet.apply {
connect(R.id.qs_frame, END, endConstraintId, END)
setMargin(R.id.qs_frame, START, if (splitShadeEnabled) 0 else panelMarginHorizontal)
setMargin(R.id.qs_frame, END, if (splitShadeEnabled) 0 else panelMarginHorizontal)
setMargin(R.id.qs_frame, TOP, topMargin)
}
}
private fun setKeyguardStatusViewConstraints(constraintSet: ConstraintSet) {
val statusViewMarginHorizontal = resources.getDimensionPixelSize(
R.dimen.status_view_margin_horizontal)
constraintSet.apply {
setMargin(R.id.keyguard_status_view, START, statusViewMarginHorizontal)
setMargin(R.id.keyguard_status_view, END, statusViewMarginHorizontal)
}
}
private fun ensureAllViewsHaveIds(parentView: ViewGroup) {
for (i in 0 until parentView.childCount) {
val childView = parentView.getChildAt(i)
if (childView.id == View.NO_ID) {
childView.id = View.generateViewId()
}
}
}
}
private data class Paddings(
val containerPadding: Int,
val notificationsMargin: Int,
val qsContainerPadding: Int
)
private fun KMutableProperty0<Int>.setAndReportChange(newValue: Int): Boolean {
val oldValue = get()
set(newValue)
return oldValue != newValue
}