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

Commit a4fd03bf authored by Massimo Carli's avatar Massimo Carli
Browse files

[85/n] Implement TaskIdResolver

When dealing with MultiWindow (e.g. split screen) the id of
the task received in a Change might not be the id of the task
associated to the related letterbox surfaces.

To handle these cases it's useful to store the parentId which
will be used to detect the right task id.

Flag: com.android.window.flags.app_compat_refactoring_fix_multiwindow_task_hierarchy
Bug: 430486865
Test: atest WMShellUnitTests:TaskIdResolverTest

Change-Id: I29e1f3a49c6352eba93143cedbe002b06915f8ff
parent d42135e4
Loading
Loading
Loading
Loading
+63 −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.wm.shell.compatui.letterbox.lifecycle

import android.app.TaskInfo
import com.android.wm.shell.compatui.letterbox.state.LetterboxTaskInfoRepository
import com.android.wm.shell.dagger.WMSingleton
import com.android.wm.shell.windowdecor.extension.isMultiWindow
import javax.inject.Inject

/**
 * The id for the Task to use in the Letterbox Lifecycle is not always the one received in
 * [TaskInfo]. Sometimes (e.g. Split Screen) the [Change] received are related to a parent [Task].
 * This class encapsulate the logic to find the right id for the [Task] used in the [Change].
 */
@WMSingleton
class TaskIdResolver @Inject constructor(
    private val letterboxTaskInfoRepository: LetterboxTaskInfoRepository
) {

    /**
     * @return The id for the Task to consider for the Letterbox bounds update.
     */
    fun getLetterboxTaskId(taskInfo: TaskInfo): Int {
        // We use the taskId itself if in the repository.
        val candidateId = taskInfo.taskId
        letterboxTaskInfoRepository.find(candidateId)?.let { item ->
            // In this case there's an item for the candidateId which means that it's an
            // eligible Task.
            return candidateId
        }
        // In this case the candidateId is not present. In case of split screen this happens when
        // the Change contains the parent of the Task with letterbox surfaces and not the Task
        // itself.
        if (taskInfo.isMultiWindow) {
            letterboxTaskInfoRepository.find { key, item -> item.parentTaskId == candidateId }
                .let { items ->
                    if (items.isNotEmpty()) {
                        // In the repository there's a Task that is eligible for letterbox surfaces
                        // whose parent has id equals to candidateId. In this case the item id
                        // will be the one. This should be exactly 1.
                        return items.first().taskId
                    }
                }
        }
        // Here the id is not present and there's no task whose parent is eligible for letterbox.
        return candidateId
    }
}
+4 −1
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.wm.shell.compatui.letterbox.state

import android.app.ActivityTaskManager
import android.view.SurfaceControl
import android.window.WindowContainerToken
import com.android.internal.protolog.ProtoLog
@@ -30,7 +31,9 @@ import javax.inject.Inject
 */
data class LetterboxTaskInfoState(
    val containerToken: WindowContainerToken,
    val containerLeash: SurfaceControl
    val containerLeash: SurfaceControl,
    val taskId: Int = ActivityTaskManager.INVALID_TASK_ID,
    val parentTaskId: Int = ActivityTaskManager.INVALID_TASK_ID
)

/**
+151 −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.wm.shell.compatui.letterbox.lifecycle

import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
import android.testing.AndroidTestingRunner
import android.view.SurfaceControl
import android.window.WindowContainerToken
import androidx.test.filters.SmallTest
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.compatui.letterbox.state.LetterboxTaskInfoRepository
import com.android.wm.shell.compatui.letterbox.state.LetterboxTaskInfoState
import com.android.wm.shell.util.testTaskIdResolver
import java.util.function.Consumer
import kotlin.test.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock

/**
 * Tests for [TaskIdResolver].
 *
 * Build/Install/Run:
 *  atest WMShellUnitTests:TaskIdResolverTest
 */
@RunWith(AndroidTestingRunner::class)
@SmallTest
class TaskIdResolverTest : ShellTestCase() {

    @Test
    fun `Returns the same id when the task is present in the repository`() {
        runTestScenario { r ->
            testTaskIdResolver(r::factory) {
                r.prepareRepository { repo ->
                    repo.insert(
                        key = 10,
                        r.createItem(
                            taskId = 10,
                        )
                    )
                }
                runningTaskInfo { ti ->
                    ti.taskId = 10
                }
                verifyLetterboxTaskId { letterboxTaskId ->
                    assertEquals(10, letterboxTaskId)
                }
            }
        }
    }

    @Test
    fun `InMultiWindow returns id of the task with given task as parent task`() {
        runTestScenario { r ->
            testTaskIdResolver(r::factory) {
                r.prepareRepository { repo ->
                    repo.insert(
                        key = 20,
                        r.createItem(
                            taskId = 20,
                            parentTaskId = 10
                        )
                    )
                }
                runningTaskInfo { ti ->
                    ti.taskId = 10
                    ti.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_MULTI_WINDOW
                }
                verifyLetterboxTaskId { letterboxTaskId ->
                    assertEquals(20, letterboxTaskId)
                }
            }
        }
    }

    @Test
    fun `InMultiwindow returns same id if there is no task with task id as parent id`() {
        runTestScenario { r ->
            testTaskIdResolver(r::factory) {
                r.prepareRepository { repo ->
                    repo.insert(
                        key = 20,
                        r.createItem(
                            taskId = 20,
                            parentTaskId = 30
                        )
                    )
                }
                runningTaskInfo { ti ->
                    ti.taskId = 10
                    ti.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_MULTI_WINDOW
                }
                verifyLetterboxTaskId { letterboxTaskId ->
                    assertEquals(10, letterboxTaskId)
                }
            }
        }
    }

    /**
     * Runs a test scenario providing a Robot.
     */
    fun runTestScenario(consumer: Consumer<TaskIdResolverRobotTest>) {
        consumer.accept(TaskIdResolverRobotTest())
    }

    class TaskIdResolverRobotTest {

        companion object {
            @JvmStatic
            val TEST_TOKEN = mock<WindowContainerToken>()

            @JvmStatic
            val TEST_LEASH = mock<SurfaceControl>()
        }

        private val letterboxTaskInfoRepository: LetterboxTaskInfoRepository =
            LetterboxTaskInfoRepository()

        fun factory(): TaskIdResolver = TaskIdResolver(letterboxTaskInfoRepository)

        fun prepareRepository(consumer: (LetterboxTaskInfoRepository) -> Unit) {
            consumer(letterboxTaskInfoRepository)
        }

        fun createItem(
            taskId: Int = -1,
            parentTaskId: Int = -1
        ) =
            LetterboxTaskInfoState(
                containerToken = TEST_TOKEN,
                containerLeash = TEST_LEASH,
                taskId = taskId,
                parentTaskId = parentTaskId
            )
    }
}
+20 −1
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.wm.shell.util

import android.app.ActivityManager.RunningTaskInfo
import android.app.TaskInfo
import android.content.ComponentName
import android.graphics.Point
import android.graphics.Rect
@@ -24,7 +25,6 @@ import android.view.SurfaceControl
import android.view.WindowManager.TRANSIT_NONE
import android.view.WindowManager.TransitionFlags
import android.view.WindowManager.TransitionType

import android.window.ActivityTransitionInfo
import android.window.AppCompatTransitionInfo
import android.window.TransitionInfo
@@ -57,6 +57,25 @@ open class BaseChangeTestContext {
    }
}

/**
 * Base class for Test Contexts requiring a [TaskInfo] object.
 */
open class BaseRunningTaskInfoTestContext {

    protected lateinit var taskInfo: RunningTaskInfo

    fun runningTaskInfo(
        builder: RunningTaskInfoTestInputBuilder.(RunningTaskInfo) -> Unit
    ): RunningTaskInfo {
        val runningTaskInfoObj = RunningTaskInfoTestInputBuilder()
        return RunningTaskInfo().also {
            runningTaskInfoObj.builder(it)
        }.apply {
            taskInfo = this
        }
    }
}

/**
 * [InputBuilder] that helps in the creation of a [Change] object for testing.
 */
+44 −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.wm.shell.util

import com.android.wm.shell.compatui.letterbox.lifecycle.TaskIdResolver

@DslMarker
annotation class TaskIdResolverTagMarker

@TaskIdResolverTagMarker
class TaskIdResolverTestContext(
    private val taskIdResolver: TaskIdResolver
) : BaseRunningTaskInfoTestContext() {

    fun verifyLetterboxTaskId(consumer: (Int) -> Unit) {
        consumer(taskIdResolver.getLetterboxTaskId(taskInfo))
    }
}

/**
 * Function to run tests for the different [TaskIdResolver] implementations.
 */
fun testTaskIdResolver(
    factory: () -> TaskIdResolver,
    init: TaskIdResolverTestContext.() -> Unit
): TaskIdResolverTestContext {
    val testContext = TaskIdResolverTestContext(factory())
    testContext.init()
    return testContext
}