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

Commit 20a86b6c authored by Ats Jenk's avatar Ats Jenk
Browse files

Controller to manage launch adjacent handling

Bug: 279585872
Test: atest LaunchAdjacentControllerTest
Change-Id: Ic02c865c6ba4f7c90757c6a496eb0a51ec4cd742
parent 58143f20
Loading
Loading
Loading
Loading
+90 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.common

import android.window.WindowContainerToken
import android.window.WindowContainerTransaction
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG
import com.android.wm.shell.util.KtProtoLog

/**
 * Controller to manage behavior of activities launched with
 * [android.content.Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT].
 */
class LaunchAdjacentController(private val syncQueue: SyncTransactionQueue) {

    /** Allows to temporarily disable launch adjacent handling */
    var launchAdjacentEnabled: Boolean = true
        set(value) {
            if (field != value) {
                KtProtoLog.d(WM_SHELL_TASK_ORG, "set launch adjacent flag root enabled=%b", value)
                field = value
                container?.let { c ->
                    if (value) {
                        enableContainer(c)
                    } else {
                        disableContainer((c))
                    }
                }
            }
        }
    private var container: WindowContainerToken? = null

    /**
     * Set [container] as the new launch adjacent flag root container.
     *
     * If launch adjacent handling is disabled through [setLaunchAdjacentEnabled], won't set the
     * container until after it is enabled again.
     *
     * @see WindowContainerTransaction.setLaunchAdjacentFlagRoot
     */
    fun setLaunchAdjacentRoot(container: WindowContainerToken) {
        KtProtoLog.d(WM_SHELL_TASK_ORG, "set new launch adjacent flag root container")
        this.container = container
        if (launchAdjacentEnabled) {
            enableContainer(container)
        }
    }

    /**
     * Clear a container previously set through [setLaunchAdjacentRoot].
     *
     * Always clears the container, regardless of [launchAdjacentEnabled] value.
     *
     * @see WindowContainerTransaction.clearLaunchAdjacentFlagRoot
     */
    fun clearLaunchAdjacentRoot() {
        KtProtoLog.d(WM_SHELL_TASK_ORG, "clear launch adjacent flag root container")
        container?.let {
            disableContainer(it)
            container = null
        }
    }

    private fun enableContainer(container: WindowContainerToken) {
        KtProtoLog.v(WM_SHELL_TASK_ORG, "enable launch adjacent flag root container")
        val wct = WindowContainerTransaction()
        wct.setLaunchAdjacentFlagRoot(container)
        syncQueue.queue(wct)
    }

    private fun disableContainer(container: WindowContainerToken) {
        KtProtoLog.v(WM_SHELL_TASK_ORG, "disable launch adjacent flag root container")
        val wct = WindowContainerTransaction()
        wct.clearLaunchAdjacentFlagRoot(container)
        syncQueue.queue(wct)
    }
}
+4 −4
Original line number Diff line number Diff line
@@ -14,7 +14,7 @@
 * limitations under the License.
 */

package com.android.wm.shell.desktopmode;
package com.android.wm.shell;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -25,16 +25,16 @@ import android.window.WindowContainerToken;
/**
 * {@link WindowContainerToken} wrapper that supports a mock binder
 */
class MockToken {
public class MockToken {
    private final WindowContainerToken mToken;

    MockToken() {
    public MockToken() {
        mToken = mock(WindowContainerToken.class);
        IBinder binder = mock(IBinder.class);
        when(mToken.asBinder()).thenReturn(binder);
    }

    WindowContainerToken token() {
    public WindowContainerToken token() {
        return mToken;
    }
}
+172 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.common

import android.os.IBinder
import android.testing.AndroidTestingRunner
import android.window.WindowContainerTransaction
import android.window.WindowContainerTransaction.HierarchyOp
import androidx.test.filters.SmallTest
import com.android.wm.shell.MockToken
import com.android.wm.shell.ShellTestCase
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Mock
import org.mockito.Mockito.any
import org.mockito.Mockito.atLeastOnce
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.never
import org.mockito.Mockito.verify

@SmallTest
@RunWith(AndroidTestingRunner::class)
class LaunchAdjacentControllerTest : ShellTestCase() {

    private lateinit var controller: LaunchAdjacentController

    @Mock private lateinit var syncQueue: SyncTransactionQueue

    @Before
    fun setUp() {
        controller = LaunchAdjacentController(syncQueue)
    }

    @Test
    fun newInstance_enabledByDefault() {
        assertThat(controller.launchAdjacentEnabled).isTrue()
    }

    @Test
    fun setLaunchAdjacentRoot_launchAdjacentEnabled_setsFlagRoot() {
        val token = MockToken().token()
        controller.setLaunchAdjacentRoot(token)
        val wct = getLatestTransactionOrFail()
        assertThat(wct.getSetLaunchAdjacentFlagRootContainer()).isEqualTo(token.asBinder())
    }

    @Test
    fun setLaunchAdjacentRoot_launchAdjacentDisabled_doesNotUpdateFlagRoot() {
        val token = MockToken().token()
        controller.launchAdjacentEnabled = false
        controller.setLaunchAdjacentRoot(token)
        verify(syncQueue, never()).queue(any())
    }

    @Test
    fun clearLaunchAdjacentRoot_launchAdjacentEnabled_clearsFlagRoot() {
        val token = MockToken().token()
        controller.setLaunchAdjacentRoot(token)
        controller.clearLaunchAdjacentRoot()
        val wct = getLatestTransactionOrFail()
        assertThat(wct.getClearLaunchAdjacentFlagRootContainer()).isEqualTo(token.asBinder())
    }

    @Test
    fun clearLaunchAdjacentRoot_launchAdjacentDisabled_clearsFlagRoot() {
        val token = MockToken().token()
        controller.setLaunchAdjacentRoot(token)
        controller.launchAdjacentEnabled = false
        clearInvocations(syncQueue)

        controller.clearLaunchAdjacentRoot()
        val wct = getLatestTransactionOrFail()
        assertThat(wct.getClearLaunchAdjacentFlagRootContainer()).isEqualTo(token.asBinder())
    }

    @Test
    fun setLaunchAdjacentEnabled_wasDisabledWithContainerSet_setsFlagRoot() {
        val token = MockToken().token()
        controller.setLaunchAdjacentRoot(token)
        controller.launchAdjacentEnabled = false
        clearInvocations(syncQueue)

        controller.launchAdjacentEnabled = true
        val wct = getLatestTransactionOrFail()
        assertThat(wct.getSetLaunchAdjacentFlagRootContainer()).isEqualTo(token.asBinder())
    }

    @Test
    fun setLaunchAdjacentEnabled_containerNotSet_doesNotUpdateFlagRoot() {
        controller.launchAdjacentEnabled = false
        controller.launchAdjacentEnabled = true
        verify(syncQueue, never()).queue(any())
    }

    @Test
    fun setLaunchAdjacentEnabled_multipleTimes_setsFlagRootOnce() {
        val token = MockToken().token()
        controller.setLaunchAdjacentRoot(token)
        controller.launchAdjacentEnabled = true
        controller.launchAdjacentEnabled = true
        // Only execute once
        verify(syncQueue).queue(any())
    }

    @Test
    fun setLaunchAdjacentDisabled_containerSet_clearsFlagRoot() {
        val token = MockToken().token()
        controller.setLaunchAdjacentRoot(token)
        controller.launchAdjacentEnabled = false
        val wct = getLatestTransactionOrFail()
        assertThat(wct.getClearLaunchAdjacentFlagRootContainer()).isEqualTo(token.asBinder())
    }

    @Test
    fun setLaunchAdjacentDisabled_containerNotSet_doesNotUpdateFlagRoot() {
        controller.launchAdjacentEnabled = false
        verify(syncQueue, never()).queue(any())
    }

    @Test
    fun setLaunchAdjacentDisabled_multipleTimes_setsFlagRootOnce() {
        val token = MockToken().token()
        controller.setLaunchAdjacentRoot(token)
        clearInvocations(syncQueue)
        controller.launchAdjacentEnabled = false
        controller.launchAdjacentEnabled = false
        // Only execute once
        verify(syncQueue).queue(any())
    }

    private fun getLatestTransactionOrFail(): WindowContainerTransaction {
        val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
        verify(syncQueue, atLeastOnce()).queue(arg.capture())
        return arg.allValues.last().also { assertThat(it).isNotNull() }
    }
}

private fun WindowContainerTransaction.getSetLaunchAdjacentFlagRootContainer(): IBinder {
    return hierarchyOps
        // Find the operation with the correct type
        .filter { op -> op.type == HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ADJACENT_FLAG_ROOT }
        // For set flag root operation, toTop is false
        .filter { op -> !op.toTop }
        .map { it.container }
        .first()
}

private fun WindowContainerTransaction.getClearLaunchAdjacentFlagRootContainer(): IBinder {
    return hierarchyOps
        // Find the operation with the correct type
        .filter { op -> op.type == HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ADJACENT_FLAG_ROOT }
        // For clear flag root operation, toTop is true
        .filter { op -> op.toTop }
        .map { it.container }
        .first()
}
+1 −0
Original line number Diff line number Diff line
@@ -59,6 +59,7 @@ import android.window.WindowContainerTransaction.HierarchyOp;
import androidx.test.filters.SmallTest;

import com.android.dx.mockito.inline.extended.StaticMockitoSession;
import com.android.wm.shell.MockToken;
import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.ShellTestCase;
+1 −0
Original line number Diff line number Diff line
@@ -39,6 +39,7 @@ import androidx.test.filters.SmallTest
import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
import com.android.dx.mockito.inline.extended.ExtendedMockito.never
import com.android.dx.mockito.inline.extended.StaticMockitoSession
import com.android.wm.shell.MockToken
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.ShellTestCase
Loading