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

Commit 5f479e8f authored by Massimo Carli's avatar Massimo Carli
Browse files

[22/n] Event handling for reachability

Implements ReachabilityGestureListener which receives double
tap events and handle them only if happening outside of the
letterboxed activity bounds.

When the event happens outside of the activity bounds, the
ReachabilityGestureListener triggers a WindowContainerTransaction
to start the reachability animation.

Flag: com.android.window.flags.app_compat_refactoring
Bug: 371500295
Test: atest WMShellUnitTests:ReachabilityGestureListenerFactoryTest
Test: atest WMShellUnitTests:ReachabilityGestureListenerTest
Test: atest WMShellUnitTests:WindowContainerTransactionSupplierTest

Change-Id: Iec2344994e52d719dbdc5793cc7df1c5900d5b58
parent b04e0f11
Loading
Loading
Loading
Loading
+35 −0
Original line number Diff line number Diff line
/*
 * Copyright 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.common

import android.window.WindowContainerTransaction
import com.android.wm.shell.dagger.WMSingleton
import java.util.function.Supplier
import javax.inject.Inject

/**
 * An Injectable [Supplier<WindowContainerTransaction>]. This can be used in place of kotlin default
 * parameters values [builder = ::WindowContainerTransaction] which requires the
 * [@JvmOverloads] annotation to make this available in Java.
 * This can be used every time a component needs the dependency to the default [Supplier] for
 * [WindowContainerTransaction]s.
 */
@WMSingleton
class WindowContainerTransactionSupplier @Inject constructor(
) : Supplier<WindowContainerTransaction> {
    override fun get(): WindowContainerTransaction = WindowContainerTransaction()
}
+66 −0
Original line number Diff line number Diff line
/*
 * Copyright 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.events

import android.graphics.Rect
import android.view.GestureDetector
import android.view.MotionEvent
import android.window.WindowContainerToken
import com.android.wm.shell.common.WindowContainerTransactionSupplier
import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.transition.Transitions.TRANSIT_MOVE_LETTERBOX_REACHABILITY

/**
 * [GestureDetector.SimpleOnGestureListener] implementation which receives events from the
 * Letterbox Input surface, understands the type of event and filter them based on the current
 * letterbox position.
 */
class ReachabilityGestureListener(
    private val taskId: Int,
    private val token: WindowContainerToken?,
    private val transitions: Transitions,
    private val animationHandler: Transitions.TransitionHandler,
    private val wctSupplier: WindowContainerTransactionSupplier
) : GestureDetector.SimpleOnGestureListener() {

    // The current letterbox bounds. Double tap events are ignored when happening in these bounds.
    private val activityBounds = Rect()

    override fun onDoubleTap(e: MotionEvent): Boolean {
        val x = e.rawX.toInt()
        val y = e.rawY.toInt()
        if (!activityBounds.contains(x, y)) {
            val wct = wctSupplier.get().apply {
                setReachabilityOffset(token!!, taskId, x, y)
            }
            transitions.startTransition(
                TRANSIT_MOVE_LETTERBOX_REACHABILITY,
                wct,
                animationHandler
            )
            return true
        }
        return false
    }

    /**
     * Updates the bounds for the letterboxed activity.
     */
    fun updateActivityBounds(newActivityBounds: Rect) {
        activityBounds.set(newActivityBounds)
    }
}
+43 −0
Original line number Diff line number Diff line
/*
 * Copyright 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.events

import android.window.WindowContainerToken
import com.android.wm.shell.common.WindowContainerTransactionSupplier
import com.android.wm.shell.dagger.WMSingleton
import com.android.wm.shell.transition.Transitions
import javax.inject.Inject

/**
 * A Factory for [ReachabilityGestureListener].
 */
@WMSingleton
class ReachabilityGestureListenerFactory @Inject constructor(
    private val transitions: Transitions,
    private val animationHandler: Transitions.TransitionHandler,
    private val wctSupplier: WindowContainerTransactionSupplier
) {
    /**
     * @return a [ReachabilityGestureListener] implementation to listen to double tap events and
     * creating the related [WindowContainerTransaction] to handle the transition.
     */
    fun createReachabilityGestureListener(
        taskId: Int,
        token: WindowContainerToken?
    ): ReachabilityGestureListener =
        ReachabilityGestureListener(taskId, token, transitions, animationHandler, wctSupplier)
}
+42 −0
Original line number Diff line number Diff line
/*
 * Copyright 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.common

import android.testing.AndroidTestingRunner
import android.window.WindowContainerTransaction
import androidx.test.filters.SmallTest
import org.junit.Test
import org.junit.runner.RunWith

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

    @Test
    fun `WindowContainerTransactionSupplier supplies a WindowContainerTransaction`() {
        val supplier = WindowContainerTransactionSupplier()
        SuppliersUtilsTest.assertSupplierProvidesValue(supplier) {
            it is WindowContainerTransaction
        }
    }
}
+131 −0
Original line number Diff line number Diff line
/*
 * Copyright 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.events

import android.graphics.Rect
import android.testing.AndroidTestingRunner
import android.window.WindowContainerToken
import android.window.WindowContainerTransaction
import androidx.test.filters.SmallTest
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.common.WindowContainerTransactionSupplier
import com.android.wm.shell.compatui.letterbox.LetterboxEvents.motionEventAt
import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.transition.Transitions.TRANSIT_MOVE_LETTERBOX_REACHABILITY
import java.util.function.Consumer
import kotlin.test.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify

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

    @Test
    fun `When invoked a ReachabilityGestureListenerFactory is created`() {
        runTestScenario { r ->
            r.invokeCreate()

            r.checkReachabilityGestureListenerCreated()
        }
    }

    @Test
    fun `Right parameters are used for creation`() {
        runTestScenario { r ->
            r.invokeCreate()

            r.checkRightParamsAreUsed()
        }
    }

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

    class ReachabilityGestureListenerFactoryRobotTest {

        companion object {
            @JvmStatic
            private val TASK_ID = 1

            @JvmStatic
            private val TOKEN = mock<WindowContainerToken>()
        }

        private val transitions: Transitions
        private val animationHandler: Transitions.TransitionHandler
        private val factory: ReachabilityGestureListenerFactory
        private val wctSupplier: WindowContainerTransactionSupplier
        private val wct: WindowContainerTransaction
        private lateinit var obtainedResult: Any

        init {
            transitions = mock<Transitions>()
            animationHandler = mock<Transitions.TransitionHandler>()
            wctSupplier = mock<WindowContainerTransactionSupplier>()
            wct = mock<WindowContainerTransaction>()
            doReturn(wct).`when`(wctSupplier).get()
            factory = ReachabilityGestureListenerFactory(transitions, animationHandler, wctSupplier)
        }

        fun invokeCreate(taskId: Int = TASK_ID, token: WindowContainerToken? = TOKEN) {
            obtainedResult = factory.createReachabilityGestureListener(taskId, token)
        }

        fun checkReachabilityGestureListenerCreated(expected: Boolean = true) {
            assertEquals(expected, obtainedResult is ReachabilityGestureListener)
        }

        fun checkRightParamsAreUsed(taskId: Int = TASK_ID, token: WindowContainerToken? = TOKEN) {
            with(obtainedResult as ReachabilityGestureListener) {
                // Click outside the bounds
                updateActivityBounds(Rect(0, 0, 10, 20))
                onDoubleTap(motionEventAt(50f, 100f))
                // WindowContainerTransactionSupplier is invoked to create a
                // WindowContainerTransaction
                verify(wctSupplier).get()
                // Verify the right params are passed to startAppCompatReachability()
                verify(wct).setReachabilityOffset(
                    token!!,
                    taskId,
                    50,
                    100
                )
                // startTransition() is invoked on Transitions with the right parameters
                verify(transitions).startTransition(
                    TRANSIT_MOVE_LETTERBOX_REACHABILITY,
                    wct,
                    animationHandler
                )
            }
        }
    }
}
Loading