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

Commit 1b958622 authored by Michal Brzezinski's avatar Michal Brzezinski
Browse files

Handling back gesture in BackGestureTutorialScreen

The only thing that's reacting to the gesture now is title text changing to "Done"

- registering MotionEvents handler to pass to ViewModel
- GestureTutorialViewModel filters events coming from touchpad
- BackGestureMonitor receives touchpad events and contains logic for recognizing the gesture

Also introducing FakeMotionEvent methods that will be helpful for testing MotionEvents

Bug: 346579074
Flag: com.android.systemui.shared.new_touchpad_gestures_tutorial
Test: manually, see video
Test: BackGestureMonitorTest
Test: GestureTutorialViewModelTest
Change-Id: I516c6d661e5d332fcccc78c21580095ddf90f4a7
parent 6f9beb9c
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -3577,6 +3577,8 @@
    <string name="touchpad_tutorial_action_key_button">Action key</string>
    <!-- Label for button finishing touchpad tutorial [CHAR LIMIT=NONE] -->
    <string name="touchpad_tutorial_done_button">Done</string>
    <!-- Screen title after gesture was done successfully [CHAR LIMIT=NONE] -->
    <string name="touchpad_tutorial_gesture_done">Great job!</string>
    <!-- Touchpad back gesture action name in tutorial [CHAR LIMIT=NONE] -->
    <string name="touchpad_back_gesture_action_title">Go back</string>
    <!-- Touchpad back gesture guidance in gestures tutorial [CHAR LIMIT=NONE] -->
+64 −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.view.MotionEvent
import kotlin.math.abs

/**
 * Monitor for touchpad gestures that calls [gestureDoneCallback] when gesture was successfully
 * done. All tracked motion events should be passed to [processTouchpadEvent]
 */
interface TouchpadGestureMonitor {

    val gestureDistanceThresholdPx: Int
    val gestureDoneCallback: () -> Unit

    fun processTouchpadEvent(event: MotionEvent)
}

class BackGestureMonitor(
    override val gestureDistanceThresholdPx: Int,
    override val gestureDoneCallback: () -> Unit
) : TouchpadGestureMonitor {

    private var xStart = 0f

    override fun processTouchpadEvent(event: MotionEvent) {
        val action = event.actionMasked
        when (action) {
            MotionEvent.ACTION_DOWN -> {
                if (isThreeFingerTouchpadSwipe(event)) {
                    xStart = event.x
                }
            }
            MotionEvent.ACTION_UP -> {
                if (isThreeFingerTouchpadSwipe(event)) {
                    val distance = abs(event.x - xStart)
                    if (distance >= gestureDistanceThresholdPx) {
                        gestureDoneCallback()
                    }
                }
            }
        }
    }

    private fun isThreeFingerTouchpadSwipe(event: MotionEvent): Boolean {
        return event.classification == MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE &&
            event.getAxisValue(MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT) == 3f
    }
}
+32 −0
Original line number Diff line number Diff line
@@ -14,26 +14,19 @@
 * limitations under the License.
 */

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

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
enum class TouchpadGesture {
    BACK,
    HOME;

sealed class GestureTutorialViewModel : ViewModel()

class BackGestureTutorialViewModel : GestureTutorialViewModel()

class HomeGestureTutorialViewModel : GestureTutorialViewModel()

class GestureViewModelFactory : ViewModelProvider.Factory {

    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return when (modelClass) {
            BackGestureTutorialViewModel::class.java -> BackGestureTutorialViewModel()
            HomeGestureTutorialViewModel::class.java -> HomeGestureTutorialViewModel()
            else -> error("Unknown ViewModel class: ${modelClass.name}")
    fun toMonitor(
        swipeDistanceThresholdPx: Int,
        gestureDoneCallback: () -> Unit
    ): TouchpadGestureMonitor {
        return when (this) {
            BACK -> BackGestureMonitor(swipeDistanceThresholdPx, gestureDoneCallback)
            else -> throw IllegalArgumentException("Not implemented yet")
        }
            as T
    }
}
+50 −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.view.InputDevice
import android.view.MotionEvent

/**
 * Allows listening to touchpadGesture and calling onDone when gesture was triggered. Can have all
 * motion events passed to [onMotionEvent] and will filter touchpad events accordingly
 */
class TouchpadGestureHandler(
    touchpadGesture: TouchpadGesture,
    swipeDistanceThresholdPx: Int,
    onDone: () -> Unit
) {

    private val gestureRecognition =
        touchpadGesture.toMonitor(swipeDistanceThresholdPx, gestureDoneCallback = onDone)

    fun onMotionEvent(event: MotionEvent): Boolean {
        // events from touchpad have SOURCE_MOUSE and not SOURCE_TOUCHPAD because of legacy reasons
        val isFromTouchpad =
            event.isFromSource(InputDevice.SOURCE_MOUSE) &&
                event.getToolType(0) == MotionEvent.TOOL_TYPE_FINGER
        val buttonClick =
            event.actionMasked == MotionEvent.ACTION_DOWN &&
                event.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
        return if (isFromTouchpad && !buttonClick) {
            gestureRecognition.processTouchpadEvent(event)
            true
        } else {
            false
        }
    }
}
+52 −14
Original line number Diff line number Diff line
@@ -17,9 +17,11 @@
package com.android.systemui.touchpad.tutorial.ui.view

import androidx.activity.compose.BackHandler
import androidx.annotation.StringRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -31,29 +33,67 @@ import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInteropFilter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.android.systemui.res.R
import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGesture.BACK
import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGestureHandler

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun BackGestureTutorialScreen(
    onDoneButtonClicked: () -> Unit,
    onBack: () -> Unit,
    modifier: Modifier = Modifier,
) {
    BackHandler { onBack() }
    BackHandler(onBack = onBack)
    var gestureDone by remember { mutableStateOf(false) }
    val swipeDistanceThresholdPx =
        with(LocalContext.current) {
            resources.getDimensionPixelSize(
                com.android.internal.R.dimen.system_gestures_distance_threshold
            )
        }
    val gestureHandler =
        remember(swipeDistanceThresholdPx) {
            TouchpadGestureHandler(BACK, swipeDistanceThresholdPx, onDone = { gestureDone = true })
        }
    Box(
        modifier =
            Modifier.fillMaxSize()
                // we need to use pointerInteropFilter because some info about touchpad gestures is
                // only available in MotionEvent
                .pointerInteropFilter(onTouchEvent = gestureHandler::onMotionEvent)
    ) {
        GestureTutorialContent(gestureDone, onDoneButtonClicked)
    }
}

@Composable
private fun GestureTutorialContent(gestureDone: Boolean, onDoneButtonClicked: () -> Unit) {
    Column(
        verticalArrangement = Arrangement.Center,
        modifier =
            modifier
            Modifier.fillMaxSize()
                .background(color = MaterialTheme.colorScheme.surfaceContainer)
                .padding(start = 48.dp, top = 124.dp, end = 48.dp, bottom = 48.dp)
                .fillMaxSize()
    ) {
        Row(modifier = Modifier.fillMaxWidth().weight(1f)) {
            TutorialDescription(modifier = Modifier.weight(1f))
            TutorialDescription(
                titleTextId =
                    if (gestureDone) R.string.touchpad_tutorial_gesture_done
                    else R.string.touchpad_back_gesture_action_title,
                bodyTextId = R.string.touchpad_back_gesture_guidance,
                modifier = Modifier.weight(1f)
            )
            Spacer(modifier = Modifier.width(76.dp))
            TutorialAnimation(modifier = Modifier.weight(1f).padding(top = 24.dp))
        }
@@ -62,17 +102,15 @@ fun BackGestureTutorialScreen(
}

@Composable
fun TutorialDescription(modifier: Modifier = Modifier) {
fun TutorialDescription(
    @StringRes titleTextId: Int,
    @StringRes bodyTextId: Int,
    modifier: Modifier = Modifier
) {
    Column(verticalArrangement = Arrangement.Top, modifier = modifier) {
        Text(
            text = stringResource(id = R.string.touchpad_back_gesture_action_title),
            style = MaterialTheme.typography.displayLarge
        )
        Text(text = stringResource(id = titleTextId), style = MaterialTheme.typography.displayLarge)
        Spacer(modifier = Modifier.height(16.dp))
        Text(
            text = stringResource(id = R.string.touchpad_back_gesture_guidance),
            style = MaterialTheme.typography.bodyLarge
        )
        Text(text = stringResource(id = bodyTextId), style = MaterialTheme.typography.bodyLarge)
    }
}

Loading