diff options
10 files changed, 217 insertions, 36 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt index faf736a543dd..6feada1c9769 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt @@ -77,6 +77,8 @@ class FakeHomeStatusBarViewModel( override val iconBlockList: MutableStateFlow<List<String>> = MutableStateFlow(listOf()) + override val contentArea = MutableStateFlow(Rect(0, 0, 1, 1)) + val darkRegions = mutableListOf<Rect>() var darkIconTint = Color.BLACK diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt index a70b777a25f2..e95bc3378423 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt @@ -26,10 +26,13 @@ import android.content.testableContext import android.graphics.Rect import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags +import android.view.Display.DEFAULT_DISPLAY import android.view.View import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.display.data.repository.displayRepository +import com.android.systemui.display.data.repository.fake import com.android.systemui.flags.DisableSceneContainer import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository @@ -85,6 +88,7 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.UnconfinedTestDispatcher import org.junit.Before import org.junit.Test @@ -104,6 +108,9 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { setUpPackageManagerForMediaProjection(kosmos) } + @Before + fun addDisplays() = runBlocking { kosmos.displayRepository.fake.addDisplay(DEFAULT_DISPLAY) } + @Test fun isTransitioningFromLockscreenToOccluded_started_isTrue() = kosmos.runTest { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt index 69ef09d8bf5e..b0fa9d842480 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt @@ -25,6 +25,7 @@ import android.widget.DateTimeView import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView +import androidx.annotation.UiThread import com.android.systemui.common.ui.binder.IconViewBinder import com.android.systemui.res.R import com.android.systemui.statusbar.StatusBarIconView @@ -38,24 +39,24 @@ import com.android.systemui.statusbar.notification.icon.ui.viewbinder.Notificati /** Binder for ongoing activity chip views. */ object OngoingActivityChipBinder { /** Binds the given [chipModel] data to the given [chipView]. */ - fun bind(chipModel: OngoingActivityChipModel, chipView: View, iconViewStore: IconViewStore?) { - val chipContext = chipView.context - val chipDefaultIconView: ImageView = - chipView.requireViewById(R.id.ongoing_activity_chip_icon) - val chipTimeView: ChipChronometer = - chipView.requireViewById(R.id.ongoing_activity_chip_time) - val chipTextView: TextView = chipView.requireViewById(R.id.ongoing_activity_chip_text) - val chipShortTimeDeltaView: DateTimeView = - chipView.requireViewById(R.id.ongoing_activity_chip_short_time_delta) - val chipBackgroundView: ChipBackgroundContainer = - chipView.requireViewById(R.id.ongoing_activity_chip_background) + fun bind( + chipModel: OngoingActivityChipModel, + viewBinding: OngoingActivityChipViewBinding, + iconViewStore: IconViewStore?, + ) { + val chipContext = viewBinding.rootView.context + val chipDefaultIconView = viewBinding.defaultIconView + val chipTimeView = viewBinding.timeView + val chipTextView = viewBinding.textView + val chipShortTimeDeltaView = viewBinding.shortTimeDeltaView + val chipBackgroundView = viewBinding.backgroundView when (chipModel) { is OngoingActivityChipModel.Shown -> { // Data setChipIcon(chipModel, chipBackgroundView, chipDefaultIconView, iconViewStore) setChipMainContent(chipModel, chipTextView, chipTimeView, chipShortTimeDeltaView) - chipView.setOnClickListener(chipModel.onClickListener) + viewBinding.rootView.setOnClickListener(chipModel.onClickListener) updateChipPadding( chipModel, chipBackgroundView, @@ -65,7 +66,7 @@ object OngoingActivityChipBinder { ) // Accessibility - setChipAccessibility(chipModel, chipView, chipBackgroundView) + setChipAccessibility(chipModel, viewBinding.rootView, chipBackgroundView) // Colors val textColor = chipModel.colors.text(chipContext) @@ -83,6 +84,85 @@ object OngoingActivityChipBinder { } } + /** Stores [rootView] and relevant child views in an object for easy reference. */ + fun createBinding(rootView: View): OngoingActivityChipViewBinding { + return OngoingActivityChipViewBinding( + rootView = rootView, + timeView = rootView.requireViewById(R.id.ongoing_activity_chip_time), + textView = rootView.requireViewById(R.id.ongoing_activity_chip_text), + shortTimeDeltaView = + rootView.requireViewById(R.id.ongoing_activity_chip_short_time_delta), + defaultIconView = rootView.requireViewById(R.id.ongoing_activity_chip_icon), + backgroundView = rootView.requireViewById(R.id.ongoing_activity_chip_background), + ) + } + + /** + * Resets any width restrictions that were placed on the primary chip's contents. + * + * Should be used when the user's screen bounds changed because there may now be more room in + * the status bar to show additional content. + */ + fun resetPrimaryChipWidthRestrictions( + primaryChipViewBinding: OngoingActivityChipViewBinding, + currentPrimaryChipViewModel: OngoingActivityChipModel, + ) { + if (currentPrimaryChipViewModel is OngoingActivityChipModel.Hidden) { + return + } + resetChipMainContentWidthRestrictions( + primaryChipViewBinding, + currentPrimaryChipViewModel as OngoingActivityChipModel.Shown, + ) + } + + /** + * Resets any width restrictions that were placed on the secondary chip and its contents. + * + * Should be used when the user's screen bounds changed because there may now be more room in + * the status bar to show additional content. + */ + fun resetSecondaryChipWidthRestrictions( + secondaryChipViewBinding: OngoingActivityChipViewBinding, + currentSecondaryChipModel: OngoingActivityChipModel, + ) { + if (currentSecondaryChipModel is OngoingActivityChipModel.Hidden) { + return + } + secondaryChipViewBinding.rootView.resetWidthRestriction() + resetChipMainContentWidthRestrictions( + secondaryChipViewBinding, + currentSecondaryChipModel as OngoingActivityChipModel.Shown, + ) + } + + private fun resetChipMainContentWidthRestrictions( + viewBinding: OngoingActivityChipViewBinding, + model: OngoingActivityChipModel.Shown, + ) { + when (model) { + is OngoingActivityChipModel.Shown.Text -> viewBinding.textView.resetWidthRestriction() + is OngoingActivityChipModel.Shown.Timer -> viewBinding.timeView.resetWidthRestriction() + is OngoingActivityChipModel.Shown.ShortTimeDelta -> + viewBinding.shortTimeDeltaView.resetWidthRestriction() + is OngoingActivityChipModel.Shown.IconOnly, + is OngoingActivityChipModel.Shown.Countdown -> {} + } + } + + /** + * Resets any width restrictions that were placed on the given view. + * + * Should be used when the user's screen bounds changed because there may now be more room in + * the status bar to show additional content. + */ + @UiThread + fun View.resetWidthRestriction() { + // View needs to be visible in order to be re-measured + visibility = View.VISIBLE + forceLayout() + } + private fun setChipIcon( chipModel: OngoingActivityChipModel.Shown, backgroundView: ChipBackgroundContainer, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipViewBinding.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipViewBinding.kt new file mode 100644 index 000000000000..1814b7430330 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipViewBinding.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2025 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.statusbar.chips.ui.binder + +import android.view.View +import android.widget.ImageView +import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer +import com.android.systemui.statusbar.chips.ui.view.ChipChronometer +import com.android.systemui.statusbar.chips.ui.view.ChipDateTimeView +import com.android.systemui.statusbar.chips.ui.view.ChipTextView + +/** Stores bound views for a given chip. */ +data class OngoingActivityChipViewBinding( + val rootView: View, + val timeView: ChipChronometer, + val textView: ChipTextView, + val shortTimeDeltaView: ChipDateTimeView, + val defaultIconView: ImageView, + val backgroundView: ChipBackgroundContainer, +) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/view/ChipChronometer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/view/ChipChronometer.kt index ff3061e850d9..7b4b79d7c852 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/view/ChipChronometer.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/view/ChipChronometer.kt @@ -33,10 +33,8 @@ import androidx.annotation.UiThread * that wide. This means the chip may get larger over time (e.g. in the transition from 59:59 to * 1:00:00), but never smaller. * 2) Hiding the text if the time gets too long for the space available. Once the text has been - * hidden, it remains hidden for the duration of the activity. - * - * Note that if the text was too big in portrait mode, resulting in the text being hidden, then the - * text will also be hidden in landscape (even if there is enough space for it in landscape). + * hidden, it remains hidden for the duration of the activity (or until [resetWidthRestriction] + * is called). */ class ChipChronometer @JvmOverloads @@ -51,12 +49,23 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : private var shouldHideText: Boolean = false override fun setBase(base: Long) { - // These variables may have changed during the previous activity, so re-set them before the - // new activity starts. + resetWidthRestriction() + super.setBase(base) + } + + /** + * Resets any width restrictions that were placed on the chronometer. + * + * Should be used when the user's screen bounds changed because there may now be more room in + * the status bar to show additional content. + */ + @UiThread + fun resetWidthRestriction() { minimumTextWidth = 0 shouldHideText = false + // View needs to be visible in order to be re-measured visibility = VISIBLE - super.setBase(base) + forceLayout() } /** Sets whether this view should hide its text or not. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt index 2541d84a5a97..31d6d86d1b37 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt @@ -45,6 +45,7 @@ import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernizat import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.VisibilityModel import javax.inject.Inject +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch /** @@ -120,22 +121,26 @@ constructor( !StatusBarNotifChips.isEnabled && !StatusBarChipsModernization.isEnabled ) { - val primaryChipView: View = - view.requireViewById(R.id.ongoing_activity_chip_primary) + val primaryChipViewBinding = + OngoingActivityChipBinder.createBinding( + view.requireViewById(R.id.ongoing_activity_chip_primary) + ) launch { viewModel.primaryOngoingActivityChip.collect { primaryChipModel -> OngoingActivityChipBinder.bind( primaryChipModel, - primaryChipView, + primaryChipViewBinding, iconViewStore, ) if (StatusBarRootModernization.isEnabled) { when (primaryChipModel) { is OngoingActivityChipModel.Shown -> - primaryChipView.show(shouldAnimateChange = true) + primaryChipViewBinding.rootView.show( + shouldAnimateChange = true + ) is OngoingActivityChipModel.Hidden -> - primaryChipView.hide( + primaryChipViewBinding.rootView.hide( state = View.GONE, shouldAnimateChange = primaryChipModel.shouldAnimate, ) @@ -166,28 +171,34 @@ constructor( StatusBarNotifChips.isEnabled && !StatusBarChipsModernization.isEnabled ) { - val primaryChipView: View = - view.requireViewById(R.id.ongoing_activity_chip_primary) - val secondaryChipView: View = - view.requireViewById(R.id.ongoing_activity_chip_secondary) + // Create view bindings here so we don't keep re-fetching child views each time + // the chip model changes. + val primaryChipViewBinding = + OngoingActivityChipBinder.createBinding( + view.requireViewById(R.id.ongoing_activity_chip_primary) + ) + val secondaryChipViewBinding = + OngoingActivityChipBinder.createBinding( + view.requireViewById(R.id.ongoing_activity_chip_secondary) + ) launch { - viewModel.ongoingActivityChips.collect { chips -> + viewModel.ongoingActivityChips.collectLatest { chips -> OngoingActivityChipBinder.bind( chips.primary, - primaryChipView, + primaryChipViewBinding, iconViewStore, ) - // TODO(b/364653005): Don't show the secondary chip if there isn't - // enough space for it. OngoingActivityChipBinder.bind( chips.secondary, - secondaryChipView, + secondaryChipViewBinding, iconViewStore, ) if (StatusBarRootModernization.isEnabled) { - primaryChipView.adjustVisibility(chips.primary.toVisibilityModel()) - secondaryChipView.adjustVisibility( + primaryChipViewBinding.rootView.adjustVisibility( + chips.primary.toVisibilityModel() + ) + secondaryChipViewBinding.rootView.adjustVisibility( chips.secondary.toVisibilityModel() ) } else { @@ -200,6 +211,18 @@ constructor( shouldAnimate = true, ) } + + viewModel.contentArea.collect { _ -> + OngoingActivityChipBinder.resetPrimaryChipWidthRestrictions( + primaryChipViewBinding, + viewModel.ongoingActivityChips.value.primary, + ) + OngoingActivityChipBinder.resetSecondaryChipWidthRestrictions( + secondaryChipViewBinding, + viewModel.ongoingActivityChips.value.secondary, + ) + view.requestLayout() + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt index 3f701fc56ab4..d731752ad5d5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt @@ -43,6 +43,7 @@ import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationSt import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.Idle import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel import com.android.systemui.statusbar.featurepods.popups.ui.viewmodel.StatusBarPopupChipsViewModel +import com.android.systemui.statusbar.layout.ui.viewmodel.StatusBarContentInsetsViewModelStore import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor import com.android.systemui.statusbar.notification.headsup.PinnedStatus @@ -130,6 +131,9 @@ interface HomeStatusBarViewModel { /** Which icons to block from the home status bar */ val iconBlockList: Flow<List<String>> + /** This status bar's current content area for the given rotation in absolute bounds. */ + val contentArea: Flow<Rect> + /** * Apps can request a low profile mode [android.view.View.SYSTEM_UI_FLAG_LOW_PROFILE] where * status bar and navigation icons dim. In this mode, a notification dot appears where the @@ -185,6 +189,7 @@ constructor( ongoingActivityChipsViewModel: OngoingActivityChipsViewModel, statusBarPopupChipsViewModel: StatusBarPopupChipsViewModel, animations: SystemStatusEventAnimationInteractor, + statusBarContentInsetsViewModelStore: StatusBarContentInsetsViewModelStore, @Application coroutineScope: CoroutineScope, ) : HomeStatusBarViewModel { override val isTransitioningFromLockscreenToOccluded: StateFlow<Boolean> = @@ -363,6 +368,10 @@ constructor( override val iconBlockList: Flow<List<String>> = homeStatusBarIconBlockListInteractor.iconBlockList + override val contentArea: Flow<Rect> = + statusBarContentInsetsViewModelStore.forDisplay(thisDisplayId)?.contentArea + ?: flowOf(Rect(0, 0, 0, 0)) + @View.Visibility private fun Boolean.toVisibleOrGone(): Int { return if (this) View.VISIBLE else View.GONE diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt index 3fc60e339543..a64fc2413246 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt @@ -115,3 +115,6 @@ class FakeDisplayRepository @Inject constructor() : DisplayRepository { interface FakeDisplayRepositoryModule { @Binds fun bindFake(fake: FakeDisplayRepository): DisplayRepository } + +val DisplayRepository.fake + get() = this as FakeDisplayRepository diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelKosmos.kt index 433bd5bc8bb3..889d469489f2 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelKosmos.kt @@ -16,8 +16,20 @@ package com.android.systemui.statusbar.layout.ui.viewmodel +import com.android.systemui.display.data.repository.displayRepository import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.statusbar.data.repository.multiDisplayStatusBarContentInsetsProviderStore import com.android.systemui.statusbar.layout.statusBarContentInsetsProvider val Kosmos.statusBarContentInsetsViewModel by Kosmos.Fixture { StatusBarContentInsetsViewModel(statusBarContentInsetsProvider) } + +val Kosmos.multiDisplayStatusBarContentInsetsViewModelStore by + Kosmos.Fixture { + MultiDisplayStatusBarContentInsetsViewModelStore( + applicationCoroutineScope, + displayRepository, + multiDisplayStatusBarContentInsetsProviderStore, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt index 5db0d5a25d83..db7e31bb2cb6 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt @@ -27,6 +27,7 @@ import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.statusbar.chips.ui.viewmodel.ongoingActivityChipsViewModel import com.android.systemui.statusbar.events.domain.interactor.systemStatusEventAnimationInteractor import com.android.systemui.statusbar.featurepods.popups.ui.viewmodel.statusBarPopupChipsViewModel +import com.android.systemui.statusbar.layout.ui.viewmodel.multiDisplayStatusBarContentInsetsViewModelStore import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor import com.android.systemui.statusbar.phone.domain.interactor.darkIconInteractor @@ -53,6 +54,7 @@ var Kosmos.homeStatusBarViewModel: HomeStatusBarViewModel by ongoingActivityChipsViewModel, statusBarPopupChipsViewModel, systemStatusEventAnimationInteractor, + multiDisplayStatusBarContentInsetsViewModelStore, applicationCoroutineScope, ) } |