/* * 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.systemui.deviceentry.ui.view import android.content.Context import android.util.AttributeSet import android.view.MotionEvent import android.view.View import com.android.systemui.deviceentry.ui.viewmodel.DeviceEntryUdfpsAccessibilityOverlayViewModel import com.android.systemui.deviceentry.ui.viewmodel.UdfpsAccessibilityOverlayViewModel /** Overlay to handle under-fingerprint sensor accessibility events. */ class UdfpsAccessibilityOverlay(context: Context?, attrs: AttributeSet? = null) : View(context, attrs) { lateinit var udfpsAccessibilityOverlayViewModel: UdfpsAccessibilityOverlayViewModel private var mLastHoverEnterEventTime: Long = -1 private val tapDurationThreshold: Long = 600 init { accessibilityLiveRegion = ACCESSIBILITY_LIVE_REGION_ASSERTIVE importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO isClickable = false } /** * The UDFPS accessibility overlay should only react to motion events when the user's finger is * in contact with the view, and should be unfocusable with a null content description whenever * the user's finger is no longer in contact with the view. * * On ACTION_HOVER_ENTER (tap or drag gesture on view) or ACTION_HOVER_MOVE (user is performing * long drag gesture across view), we mark the view IMPORTANT_FOR_ACCESSIBILITY_YES to enable * the accessibilityLiveRegion, and update the content description of the view to the * appropriate UDFPS guidance message. * * On ACTION_HOVER_EXIT, (user's finger is lifted from view, or performs drag gesture out of * bounds of the view), we mark the view IMPORTANT_FOR_ACCESSIBILITY_NO to disable the * accessibilityLiveRegion, and clear the content description. This is to disable the * accessibilityLiveRegion and linear navigation to the overlay, ensure the overlay does not * retain a stale content description, and allow Talkback to send the same UDFPS guidance * message again in sequence on the next motion event if needed. * * If the ACTION_HOVER_EXIT event was part a tap gesture (vs. a drag gesture), we add a delay in * order to prevent clearing the content description before Talkback can process the * accessibility event generated by the accessibilityLiveRegion. */ override fun dispatchGenericMotionEvent(event: MotionEvent): Boolean { if (!this::udfpsAccessibilityOverlayViewModel.isInitialized) { // ViewModel is not ready, skip processing event return true } if (event.action == MotionEvent.ACTION_HOVER_ENTER) { mLastHoverEnterEventTime = event.eventTime } if ( event.action == MotionEvent.ACTION_HOVER_ENTER || event.action == MotionEvent.ACTION_HOVER_MOVE ) { // Prepend the lockscreen content description for single taps on lock screen val includeLockscreenContentDescription = (event.eventTime - mLastHoverEnterEventTime) < 200 val udfpsHint = if ( udfpsAccessibilityOverlayViewModel is DeviceEntryUdfpsAccessibilityOverlayViewModel ) { udfpsAccessibilityOverlayViewModel .getUdfpsDirectionalFeedbackOnHoverEnterOrMove( event, includeLockscreenContentDescription, ) } else { udfpsAccessibilityOverlayViewModel .getUdfpsDirectionalFeedbackOnHoverEnterOrMove(event) } udfpsHint?.let { contentDescription = null importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_YES contentDescription = it } } else if (event.action == MotionEvent.ACTION_HOVER_EXIT) { val delay: Long = if (event.eventTime - mLastHoverEnterEventTime < tapDurationThreshold) { 600 } else { 0 } postDelayed( { importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO contentDescription = null }, delay, ) } return true } }