Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5995e29e authored by Maryam Dehaini's avatar Maryam Dehaini Committed by Android (Google) Code Review
Browse files

Merge "Move app handle bounds flow to seperate view model" into main

parents b95ddb8f 02aea488
Loading
Loading
Loading
Loading
+176 −0
Original line number Diff line number Diff line
/*
 * 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.layout.ui.viewmodel

import android.content.testableContext
import android.graphics.Rect
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.concurrency.fakeExecutor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.backgroundScope
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.statusbar.layout.StatusBarAppHandleTracking
import com.android.systemui.testKosmos
import com.android.wm.shell.windowdecor.viewholder.AppHandleIdentifier
import com.android.wm.shell.windowdecor.viewholder.AppHandleIdentifier.AppHandleWindowingMode.APP_HANDLE_WINDOWING_MODE_BUBBLE
import com.android.wm.shell.windowdecor.viewholder.AppHandleIdentifier.AppHandleWindowingMode.APP_HANDLE_WINDOWING_MODE_FULLSCREEN
import com.android.wm.shell.windowdecor.viewholder.AppHandleIdentifier.AppHandleWindowingMode.APP_HANDLE_WINDOWING_MODE_SPLIT_SCREEN
import com.google.common.truth.Truth.assertThat
import java.util.Optional
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
class AppHandlesViewModelTest : SysuiTestCase() {

    private val kosmos = testKosmos().useUnconfinedTestDispatcher()

    private val Kosmos.underTest by Kosmos.Fixture { kosmos.appHandlesViewModel }

    @Before
    fun setUp() {
        kosmos.underTest.activateIn(kosmos.testScope)
    }

    @Test
    @EnableFlags(StatusBarAppHandleTracking.FLAG_NAME)
    fun appHandleBounds_noAppHandlesProvided_empty() =
        kosmos.runTest {
            val viewModelWithNoAppHandles =
                AppHandlesViewModel(
                    backgroundScope = backgroundScope,
                    sysuiMainExecutor = fakeExecutor,
                    appHandles = Optional.empty(),
                    thisDisplayId = testableContext.displayId,
                )

            assertThat(viewModelWithNoAppHandles.appHandleBounds).isEmpty()
        }

    @Test
    @EnableFlags(StatusBarAppHandleTracking.FLAG_NAME)
    fun appHandleBounds_empty() =
        kosmos.runTest {
            fakeAppHandles.setAppHandles(emptyMap())

            assertThat(underTest.appHandleBounds).isEmpty()
        }

    @Test
    @EnableFlags(StatusBarAppHandleTracking.FLAG_NAME)
    fun appHandleBounds_notForThisDisplay_empty() =
        kosmos.runTest {
            val taskId = 10
            val rect = Rect(1, 2, 3, 4)
            fakeAppHandles.setAppHandles(
                mapOf(
                    taskId to
                        AppHandleIdentifier(
                            rect = rect,
                            displayId = testableContext.displayId + 2,
                            taskId = taskId,
                            windowingMode = APP_HANDLE_WINDOWING_MODE_FULLSCREEN,
                        )
                )
            )

            assertThat(underTest.appHandleBounds).isEmpty()
        }

    @Test
    @EnableFlags(StatusBarAppHandleTracking.FLAG_NAME)
    fun appHandleBounds_forThisDisplay_hasBounds() =
        kosmos.runTest {
            val taskId = 10
            val rect = Rect(1, 2, 3, 4)
            fakeAppHandles.setAppHandles(
                mapOf(
                    taskId to
                        AppHandleIdentifier(
                            rect = rect,
                            displayId = testableContext.displayId,
                            taskId = taskId,
                            windowingMode = APP_HANDLE_WINDOWING_MODE_FULLSCREEN,
                        )
                )
            )

            assertThat(underTest.appHandleBounds).containsExactly(rect)
        }

    @Test
    @EnableFlags(StatusBarAppHandleTracking.FLAG_NAME)
    fun appHandleBounds_multipleForThisDisplay_hasAll() =
        kosmos.runTest {
            val taskId1 = 10
            val rect1 = Rect(1, 2, 3, 4)

            val taskId2 = 20
            val rect2 = Rect(10, 20, 30, 40)

            fakeAppHandles.setAppHandles(
                mapOf(
                    taskId1 to
                        AppHandleIdentifier(
                            rect = rect1,
                            displayId = testableContext.displayId,
                            taskId = taskId1,
                            windowingMode = APP_HANDLE_WINDOWING_MODE_BUBBLE,
                        ),
                    taskId2 to
                        AppHandleIdentifier(
                            rect = rect2,
                            displayId = testableContext.displayId,
                            taskId = taskId2,
                            windowingMode = APP_HANDLE_WINDOWING_MODE_SPLIT_SCREEN,
                        ),
                )
            )

            assertThat(underTest.appHandleBounds).containsExactly(rect1, rect2)
        }

    @Test
    @DisableFlags(StatusBarAppHandleTracking.FLAG_NAME)
    fun appHandleBounds_emptyIfFlagDisabled() =
        kosmos.runTest {
            val taskId = 10
            val rect = Rect(1, 2, 3, 4)
            fakeAppHandles.setAppHandles(
                mapOf(
                    taskId to
                        AppHandleIdentifier(
                            rect = rect,
                            displayId = testableContext.displayId,
                            taskId = taskId,
                            windowingMode = APP_HANDLE_WINDOWING_MODE_FULLSCREEN,
                        )
                )
            )

            assertThat(underTest.appHandleBounds).isEmpty()
        }
}
+0 −132
Original line number Diff line number Diff line
@@ -16,30 +16,19 @@

package com.android.systemui.statusbar.layout.ui.viewmodel

import android.content.testableContext
import android.graphics.Rect
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
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.concurrency.fakeExecutor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.backgroundScope
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.statusbar.layout.StatusBarAppHandleTracking
import com.android.systemui.statusbar.policy.Clock
import com.android.systemui.testKosmos
import com.android.wm.shell.windowdecor.viewholder.AppHandleIdentifier
import com.android.wm.shell.windowdecor.viewholder.AppHandleIdentifier.AppHandleWindowingMode.APP_HANDLE_WINDOWING_MODE_BUBBLE
import com.android.wm.shell.windowdecor.viewholder.AppHandleIdentifier.AppHandleWindowingMode.APP_HANDLE_WINDOWING_MODE_FULLSCREEN
import com.android.wm.shell.windowdecor.viewholder.AppHandleIdentifier.AppHandleWindowingMode.APP_HANDLE_WINDOWING_MODE_SPLIT_SCREEN
import com.google.common.truth.Truth.assertThat
import java.util.Optional
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -150,125 +139,4 @@ class StatusBarBoundsViewModelTest : SysuiTestCase() {

            assertThat(underTest.clockBounds).isEqualTo(newRect)
        }

    @Test
    @EnableFlags(StatusBarAppHandleTracking.FLAG_NAME)
    fun appHandleBounds_noAppHandlesProvided_empty() =
        kosmos.runTest {
            val viewModelWithNoAppHandles =
                StatusBarBoundsViewModel(
                    backgroundScope = backgroundScope,
                    sysuiMainExecutor = fakeExecutor,
                    appHandles = Optional.empty(),
                    thisDisplayId = testableContext.displayId,
                    startSideContainerView = mockStatusBarStartSideContainerView,
                    clockView = mockStatusBarClockView,
                )

            assertThat(viewModelWithNoAppHandles.appHandleBounds).isEmpty()
        }

    @Test
    @EnableFlags(StatusBarAppHandleTracking.FLAG_NAME)
    fun appHandleBounds_empty() =
        kosmos.runTest {
            fakeAppHandles.setAppHandles(emptyMap())

            assertThat(underTest.appHandleBounds).isEmpty()
        }

    @Test
    @EnableFlags(StatusBarAppHandleTracking.FLAG_NAME)
    fun appHandleBounds_notForThisDisplay_empty() =
        kosmos.runTest {
            val taskId = 10
            val rect = Rect(1, 2, 3, 4)
            fakeAppHandles.setAppHandles(
                mapOf(
                    taskId to
                        AppHandleIdentifier(
                            rect = rect,
                            displayId = testableContext.displayId + 2,
                            taskId = taskId,
                            windowingMode = APP_HANDLE_WINDOWING_MODE_FULLSCREEN,
                        )
                )
            )

            assertThat(underTest.appHandleBounds).isEmpty()
        }

    @Test
    @EnableFlags(StatusBarAppHandleTracking.FLAG_NAME)
    fun appHandleBounds_forThisDisplay_hasBounds() =
        kosmos.runTest {
            val taskId = 10
            val rect = Rect(1, 2, 3, 4)
            fakeAppHandles.setAppHandles(
                mapOf(
                    taskId to
                        AppHandleIdentifier(
                            rect = rect,
                            displayId = testableContext.displayId,
                            taskId = taskId,
                            windowingMode = APP_HANDLE_WINDOWING_MODE_FULLSCREEN,
                        )
                )
            )

            assertThat(underTest.appHandleBounds).containsExactly(rect)
        }

    @Test
    @EnableFlags(StatusBarAppHandleTracking.FLAG_NAME)
    fun appHandleBounds_multipleForThisDisplay_hasAll() =
        kosmos.runTest {
            val taskId1 = 10
            val rect1 = Rect(1, 2, 3, 4)

            val taskId2 = 20
            val rect2 = Rect(10, 20, 30, 40)

            fakeAppHandles.setAppHandles(
                mapOf(
                    taskId1 to
                        AppHandleIdentifier(
                            rect = rect1,
                            displayId = testableContext.displayId,
                            taskId = taskId1,
                            windowingMode = APP_HANDLE_WINDOWING_MODE_BUBBLE,
                        ),
                    taskId2 to
                        AppHandleIdentifier(
                            rect = rect2,
                            displayId = testableContext.displayId,
                            taskId = taskId2,
                            windowingMode = APP_HANDLE_WINDOWING_MODE_SPLIT_SCREEN,
                        ),
                )
            )

            assertThat(underTest.appHandleBounds).containsExactly(rect1, rect2)
        }

    @Test
    @DisableFlags(StatusBarAppHandleTracking.FLAG_NAME)
    fun appHandleBounds_emptyIfFlagDisabled() =
        kosmos.runTest {
            val taskId = 10
            val rect = Rect(1, 2, 3, 4)
            fakeAppHandles.setAppHandles(
                mapOf(
                    taskId to
                        AppHandleIdentifier(
                            rect = rect,
                            displayId = testableContext.displayId,
                            taskId = taskId,
                            windowingMode = APP_HANDLE_WINDOWING_MODE_FULLSCREEN,
                        )
                )
            )

            assertThat(underTest.appHandleBounds).isEmpty()
        }
}
+7 −1
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChip
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.Idle
import com.android.systemui.statusbar.featurepods.popups.ui.model.PopupChipModel
import com.android.systemui.statusbar.layout.ui.viewmodel.AppHandlesViewModel
import com.android.systemui.statusbar.layout.ui.viewmodel.StatusBarBoundsViewModel
import com.android.systemui.statusbar.phone.domain.interactor.IsAreaDark
import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.BatteryNextToPercentViewModel
@@ -96,12 +97,17 @@ class FakeHomeStatusBarViewModel(
    override val statusBarBoundsViewModelFactory: StatusBarBoundsViewModel.Factory =
        object : StatusBarBoundsViewModel.Factory {
            override fun create(
                displayId: Int,
                startSideContainerView: View,
                clockView: Clock,
            ): StatusBarBoundsViewModel = mock(StatusBarBoundsViewModel::class.java)
        }

    override val appHandlesViewModelFactory: AppHandlesViewModel.Factory =
        object : AppHandlesViewModel.Factory {
            override fun create(displayId: Int): AppHandlesViewModel =
                mock(AppHandlesViewModel::class.java)
        }

    override val shouldShowOperatorNameView = MutableStateFlow(false)

    override val isClockVisible =
+87 −0
Original line number Diff line number Diff line
/*
 * 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.layout.ui.viewmodel

import android.graphics.Rect
import androidx.compose.runtime.getValue
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.lifecycle.Hydrator
import com.android.systemui.statusbar.layout.StatusBarAppHandleTracking
import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.wm.shell.windowdecor.viewholder.AppHandlePositionCallback
import com.android.wm.shell.windowdecor.viewholder.AppHandles
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import java.util.Optional
import java.util.concurrent.Executor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.stateIn

/** View model for on-screen bounds of app handles overlapping with the status bar. */
class AppHandlesViewModel
@AssistedInject
constructor(
    @Assisted thisDisplayId: Int,
    appHandles: Optional<AppHandles>,
    @Background backgroundScope: CoroutineScope,
    @Main sysuiMainExecutor: Executor,
) : ExclusiveActivatable() {
    private val hydrator = Hydrator(traceName = "AppHandlesViewModel.hydrator")

    private val _appHandleBounds: Flow<List<Rect>> =
        if (StatusBarAppHandleTracking.isEnabled && appHandles.isPresent) {
                conflatedCallbackFlow {
                    val listener = AppHandlePositionCallback { handles ->
                        trySend(
                            handles.values.filter { it.displayId == thisDisplayId }.map { it.rect }
                        )
                    }
                    appHandles.get().addListener(sysuiMainExecutor, listener)
                    awaitClose { appHandles.get().removeListener(listener) }
                }
            } else {
                flowOf(emptyList())
            }
            .stateIn(backgroundScope, SharingStarted.WhileSubscribed(), emptyList())

    /**
     * The on-screen bounds where app handles are showing. Used so that we can ensure clickable
     * status bar content doesn't overlap with them. This is a hydrated value.
     */
    val appHandleBounds: List<Rect> by
        hydrator.hydratedStateOf(
            traceName = "StatusBar.appHandleBounds",
            initialValue = emptyList(),
            source = _appHandleBounds,
        )

    override suspend fun onActivated(): Nothing {
        hydrator.activate()
    }

    @AssistedFactory
    interface Factory {
        fun create(displayId: Int): AppHandlesViewModel
    }
}
+1 −42
Original line number Diff line number Diff line
@@ -20,25 +20,18 @@ import android.graphics.Rect
import android.view.View
import androidx.compose.runtime.getValue
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.lifecycle.Hydrator
import com.android.systemui.statusbar.layout.StatusBarAppHandleTracking
import com.android.systemui.statusbar.policy.Clock
import com.android.systemui.util.boundsOnScreen
import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.wm.shell.windowdecor.viewholder.AppHandlePositionCallback
import com.android.wm.shell.windowdecor.viewholder.AppHandles
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import java.util.Optional
import java.util.concurrent.Executor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.stateIn

/**
@@ -49,12 +42,9 @@ import kotlinx.coroutines.flow.stateIn
class StatusBarBoundsViewModel
@AssistedInject
constructor(
    @Assisted thisDisplayId: Int,
    @Assisted private val startSideContainerView: View,
    @Assisted private val clockView: Clock,
    appHandles: Optional<AppHandles>,
    @Background backgroundScope: CoroutineScope,
    @Main sysuiMainExecutor: Executor,
) : ExclusiveActivatable() {
    private val hydrator = Hydrator(traceName = "StatusBarBoundsViewModel.hydrator")

@@ -100,43 +90,12 @@ constructor(
            source = _clockBounds,
        )

    private val _appHandleBounds: Flow<List<Rect>> =
        if (StatusBarAppHandleTracking.isEnabled && appHandles.isPresent) {
                conflatedCallbackFlow {
                    val listener = AppHandlePositionCallback { handles ->
                        trySend(
                            handles.values.filter { it.displayId == thisDisplayId }.map { it.rect }
                        )
                    }
                    appHandles.get().addListener(sysuiMainExecutor, listener)
                    awaitClose { appHandles.get().removeListener(listener) }
                }
            } else {
                flowOf(emptyList())
            }
            .stateIn(backgroundScope, SharingStarted.WhileSubscribed(), emptyList())

    /**
     * The on-screen bounds where app handles are showing. Used so that we can ensure clickable
     * status bar content doesn't overlap with them. This is a hydrated value.
     */
    val appHandleBounds: List<Rect> by
        hydrator.hydratedStateOf(
            traceName = "StatusBar.appHandleBounds",
            initialValue = emptyList(),
            source = _appHandleBounds,
        )

    override suspend fun onActivated(): Nothing {
        hydrator.activate()
    }

    @AssistedFactory
    interface Factory {
        fun create(
            displayId: Int,
            startSideContainerView: View,
            clockView: Clock,
        ): StatusBarBoundsViewModel
        fun create(startSideContainerView: View, clockView: Clock): StatusBarBoundsViewModel
    }
}
Loading