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

Commit 125d98c7 authored by Michał Brzeziński's avatar Michał Brzeziński Committed by Android (Google) Code Review
Browse files

Merge changes Iacf6797c,I606fa3f3,I46dd032a into main

* changes:
  1/n refactoring TouchpadGestureHandler
  1/n refactoring: renaming EasterEggGestureMonitor to EasterEggGestureRecognizer
  1/n refactoring ViewModels for touchpad tutorial
parents fbc94240 d00d46e2
Loading
Loading
Loading
Loading
+76 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.touchpad.tutorial.ui.gesture

import android.graphics.PointF
import android.view.MotionEvent
import kotlin.math.cos
import kotlin.math.sin

/** Test helper to generate circular gestures or full EasterEgg gesture */
object EasterEggGesture {

    fun motionEventsForGesture(): List<MotionEvent> {
        val gesturePath = generateCircularGesturePoints(circlesCount = 3)
        val events =
            TwoFingerGesture.eventsForFullGesture { gesturePath.forEach { p -> move(p.x, p.y) } }
        return events
    }

    /**
     * Generates list of points that would make up clockwise circular motion with given [radius].
     * [circlesCount] determines how many full circles gesture should perform. [radiusNoiseFraction]
     * can introduce noise to mimic real-world gesture which is not perfect - shape will be still
     * circular but radius at any given point can be deviate from given radius by
     * [radiusNoiseFraction].
     */
    fun generateCircularGesturePoints(
        circlesCount: Int,
        radiusNoiseFraction: Double? = null,
        radius: Float = 100f,
    ): List<PointF> {
        val pointsPerCircle = 50
        val angleStep = 360 / pointsPerCircle
        val angleBuffer = 20 // buffer to make sure we're doing a bit more than 360 degree
        val totalAngle = circlesCount * (360 + angleBuffer)
        // Because all gestures in tests should start at (DEFAULT_X, DEFAULT_Y) we need to shift
        // circle center x coordinate by radius
        val centerX = -radius
        val centerY = 0f

        val randomNoise: (Double) -> Double =
            if (radiusNoiseFraction == null) {
                { 0.0 }
            } else {
                { radianAngle -> sin(radianAngle * 2) * radiusNoiseFraction }
            }

        val events = mutableListOf<PointF>()
        var currentAngle = 0f
        // as cos(0) == 1 and sin(0) == 0 we start gesture at position of (radius, 0) and go
        // clockwise - first Y increases and X decreases
        while (currentAngle < totalAngle) {
            val radianAngle = Math.toRadians(currentAngle.toDouble())
            val radiusWithNoise = radius * (1 + randomNoise(radianAngle).toFloat())
            val x = centerX + radiusWithNoise * cos(radianAngle).toFloat()
            val y = centerY + radiusWithNoise * sin(radianAngle).toFloat()
            events.add(PointF(x, y))
            currentAngle += angleStep
        }
        return events
    }
}
+13 −57
Original line number Diff line number Diff line
@@ -16,29 +16,28 @@

package com.android.systemui.touchpad.tutorial.ui.gesture

import android.graphics.PointF
import android.view.MotionEvent
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.SWIPE_DISTANCE
import com.android.systemui.touchpad.tutorial.ui.gesture.EasterEggGesture.generateCircularGesturePoints
import com.google.common.truth.Truth.assertThat
import kotlin.math.cos
import kotlin.math.sin
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

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

    private data class Point(val x: Float, val y: Float)
class EasterEggGestureRecognizerTest : SysuiTestCase() {

    private var triggered = false
    private val handler =
        TouchpadGestureHandler(
            BackGestureRecognizer(gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt()),
            EasterEggGestureMonitor(callback = { triggered = true }),
        )
    private val gestureRecognizer = EasterEggGestureRecognizer()

    @Before
    fun setup() {
        gestureRecognizer.addGestureStateCallback { triggered = it == GestureState.Finished }
    }

    @Test
    fun easterEggTriggeredAfterThreeCircles() {
@@ -99,56 +98,13 @@ class EasterEggGestureTest : SysuiTestCase() {
    }

    private fun assertStateAfterEvents(events: List<MotionEvent>, wasTriggered: Boolean) {
        events.forEach { handler.onMotionEvent(it) }
        events.forEach { gestureRecognizer.accept(it) }
        assertThat(triggered).isEqualTo(wasTriggered)
    }

    private fun assertStateAfterTwoFingerGesture(gesturePath: List<Point>, wasTriggered: Boolean) {
    private fun assertStateAfterTwoFingerGesture(gesturePath: List<PointF>, wasTriggered: Boolean) {
        val events =
            TwoFingerGesture.eventsForFullGesture { gesturePath.forEach { (x, y) -> move(x, y) } }
            TwoFingerGesture.eventsForFullGesture { gesturePath.forEach { p -> move(p.x, p.y) } }
        assertStateAfterEvents(events = events, wasTriggered = wasTriggered)
    }

    /**
     * Generates list of points that would make up clockwise circular motion with given [radius].
     * [circlesCount] determines how many full circles gesture should perform. [radiusNoiseFraction]
     * can introduce noise to mimic real-world gesture which is not perfect - shape will be still
     * circular but radius at any given point can be deviate from given radius by
     * [radiusNoiseFraction].
     */
    private fun generateCircularGesturePoints(
        circlesCount: Int,
        radiusNoiseFraction: Double? = null,
        radius: Float = 100f,
    ): List<Point> {
        val pointsPerCircle = 50
        val angleStep = 360 / pointsPerCircle
        val angleBuffer = 20 // buffer to make sure we're doing a bit more than 360 degree
        val totalAngle = circlesCount * (360 + angleBuffer)
        // Because all gestures in tests should start at (DEFAULT_X, DEFAULT_Y) we need to shift
        // circle center x coordinate by radius
        val centerX = -radius
        val centerY = 0f

        val events = mutableListOf<Point>()
        val randomNoise: (Double) -> Double =
            if (radiusNoiseFraction == null) {
                { 0.0 }
            } else {
                { radianAngle -> sin(radianAngle * 2) * radiusNoiseFraction }
            }

        var currentAngle = 0f
        // as cos(0) == 1 and sin(0) == 0 we start gesture at position of (radius, 0) and go
        // clockwise - first Y increases and X decreases
        while (currentAngle < totalAngle) {
            val radianAngle = Math.toRadians(currentAngle.toDouble())
            val radiusWithNoise = radius * (1 + randomNoise(radianAngle).toFloat())
            val x = centerX + radiusWithNoise * cos(radianAngle).toFloat()
            val y = centerY + radiusWithNoise * sin(radianAngle).toFloat()
            events.add(Point(x, y))
            currentAngle += angleStep
        }
        return events
    }
}
+5 −21
Original line number Diff line number Diff line
@@ -33,12 +33,11 @@ import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
class TouchpadGestureHandlerTest : SysuiTestCase() {
class TouchpadEventsFilterTest : SysuiTestCase() {

    private var gestureState: GestureState = GestureState.NotStarted
    private val gestureRecognizer =
        BackGestureRecognizer(gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt())
    private val handler = TouchpadGestureHandler(gestureRecognizer, EasterEggGestureMonitor {})

    @Before
    fun before() {
@@ -48,21 +47,21 @@ class TouchpadGestureHandlerTest : SysuiTestCase() {
    @Test
    fun handlesEventsFromTouchpad() {
        val event = downEvent(source = SOURCE_MOUSE, toolType = TOOL_TYPE_FINGER)
        val eventHandled = handler.onMotionEvent(event)
        val eventHandled = gestureRecognizer.handleTouchpadMotionEvent(event)
        assertThat(eventHandled).isTrue()
    }

    @Test
    fun ignoresEventsFromMouse() {
        val event = downEvent(source = SOURCE_MOUSE, toolType = TOOL_TYPE_MOUSE)
        val eventHandled = handler.onMotionEvent(event)
        val eventHandled = gestureRecognizer.handleTouchpadMotionEvent(event)
        assertThat(eventHandled).isFalse()
    }

    @Test
    fun ignoresEventsFromTouch() {
        val event = downEvent(source = SOURCE_TOUCHSCREEN, toolType = TOOL_TYPE_FINGER)
        val eventHandled = handler.onMotionEvent(event)
        val eventHandled = gestureRecognizer.handleTouchpadMotionEvent(event)
        assertThat(eventHandled).isFalse()
    }

@@ -70,25 +69,10 @@ class TouchpadGestureHandlerTest : SysuiTestCase() {
    fun ignoresButtonClicksFromTouchpad() {
        val event = downEvent(source = SOURCE_MOUSE, toolType = TOOL_TYPE_FINGER)
        event.buttonState = MotionEvent.BUTTON_PRIMARY
        val eventHandled = handler.onMotionEvent(event)
        val eventHandled = gestureRecognizer.handleTouchpadMotionEvent(event)
        assertThat(eventHandled).isFalse()
    }

    private fun downEvent(source: Int, toolType: Int) =
        motionEvent(action = ACTION_DOWN, x = 0f, y = 0f, source = source, toolType = toolType)

    @Test
    fun triggersGestureDoneForThreeFingerGesture() {
        backGestureEvents().forEach { handler.onMotionEvent(it) }

        assertThat(gestureState).isEqualTo(GestureState.Finished)
    }

    private fun backGestureEvents(): List<MotionEvent> {
        return ThreeFingerGesture.eventsForFullGesture {
            move(deltaX = SWIPE_DISTANCE / 4)
            move(deltaX = SWIPE_DISTANCE / 2)
            move(deltaX = SWIPE_DISTANCE)
        }
    }
}
+0 −7
Original line number Diff line number Diff line
@@ -53,13 +53,6 @@ class BackGestureScreenViewModelTest : SysuiTestCase() {
        kosmos.useUnconfinedTestDispatcher()
    }

    @Test
    fun easterEggNotTriggeredAtStart() =
        kosmos.runTest {
            val easterEggTriggered by collectLastValue(viewModel.easterEggTriggered)
            assertThat(easterEggTriggered).isFalse()
        }

    @Test
    fun emitsProgressStateWithLeftProgressAnimation() =
        kosmos.runTest {
+78 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.touchpad.tutorial.ui.viewmodel

import android.view.MotionEvent
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.testKosmos
import com.android.systemui.touchpad.tutorial.ui.gesture.EasterEggGesture
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

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

    private val kosmos = testKosmos()
    private val viewModel = EasterEggGestureViewModel()

    @Before
    fun before() {
        kosmos.useUnconfinedTestDispatcher()
    }

    @Test
    fun easterEggNotTriggeredAtStart() =
        kosmos.runTest {
            val easterEggTriggered by collectLastValue(viewModel.easterEggTriggered)
            assertThat(easterEggTriggered).isFalse()
        }

    @Test
    fun emitsTrueOnEasterEggTriggered() =
        kosmos.runTest {
            assertStateAfterEvents(
                events = EasterEggGesture.motionEventsForGesture(),
                expected = true,
            )
        }

    @Test
    fun emitsFalseOnEasterEggCallbackExecuted() =
        kosmos.runTest {
            val easterEggTriggered by collectLastValue(viewModel.easterEggTriggered)
            EasterEggGesture.motionEventsForGesture().forEach { viewModel.accept(it) }

            assertThat(easterEggTriggered).isEqualTo(true)
            viewModel.onEasterEggFinished()
            assertThat(easterEggTriggered).isEqualTo(false)
        }

    private fun Kosmos.assertStateAfterEvents(events: List<MotionEvent>, expected: Boolean) {
        val state by collectLastValue(viewModel.easterEggTriggered)
        events.forEach { viewModel.accept(it) }
        assertThat(state).isEqualTo(expected)
    }
}
Loading