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

Commit 64aea7fe authored by Beverly's avatar Beverly
Browse files

Accessibility updates for UDFPS device entry

Adds a UDFPS a11y overlay on the lockscreen that takes
up the bottom half of the screen. If there are hover
events over this area when touchExploration is enabled,
there will be a11y announcements guiding the user to
the UDFPS sensor.

This CL integrates this functionality on the lockscreen
only and does not yet support the alternate bouncer.

Updates Kosmos & testing.

Flag: NONE
Bug: 310044681
Test: atest UdfpsAccessibilityOverlayViewModelTest
Change-Id: I9b5f680edc9901ba4c06bdecccef0bac7b1cb424
parent 2996824c
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -262,4 +262,8 @@

    <!--Id for the device-entry UDFPS icon that lives in the alternate bouncer. -->
    <item type="id" name="alternate_bouncer_udfps_icon_view" />

    <!-- Id for the udfps accessibility overlay -->
    <item type="id" name="udfps_accessibility_overlay" />
    <item type="id" name="udfps_accessibility_overlay_top_guideline" />
</resources>
+47 −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.systemui.deviceentry.ui.binder

import android.annotation.SuppressLint
import androidx.core.view.isInvisible
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.deviceentry.ui.view.UdfpsAccessibilityOverlay
import com.android.systemui.deviceentry.ui.viewmodel.UdfpsAccessibilityOverlayViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
import kotlinx.coroutines.ExperimentalCoroutinesApi

@ExperimentalCoroutinesApi
object UdfpsAccessibilityOverlayBinder {

    /** Forwards hover events to the view model to make guided announcements for accessibility. */
    @SuppressLint("ClickableViewAccessibility")
    @JvmStatic
    fun bind(
        view: UdfpsAccessibilityOverlay,
        viewModel: UdfpsAccessibilityOverlayViewModel,
    ) {
        view.setOnHoverListener { v, event -> viewModel.onHoverEvent(v, event) }
        view.repeatWhenAttached {
            // Repeat on CREATED because we update the visibility of the view
            repeatOnLifecycle(Lifecycle.State.CREATED) {
                viewModel.visible.collect { visible -> view.isInvisible = !visible }
            }
        }
    }
}
+23 −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.systemui.deviceentry.ui.view

import android.content.Context
import android.view.View

/** Overlay to handle under-fingerprint sensor accessibility events. */
class UdfpsAccessibilityOverlay(context: Context?) : View(context)
+91 −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.systemui.deviceentry.ui.viewmodel

import android.graphics.Point
import android.view.MotionEvent
import android.view.View
import com.android.systemui.accessibility.domain.interactor.AccessibilityInteractor
import com.android.systemui.biometrics.UdfpsUtils
import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams
import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryForegroundViewModel
import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryIconViewModel
import javax.inject.Inject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf

/** Models the UI state for the UDFPS accessibility overlay */
@ExperimentalCoroutinesApi
class UdfpsAccessibilityOverlayViewModel
@Inject
constructor(
    udfpsOverlayInteractor: UdfpsOverlayInteractor,
    accessibilityInteractor: AccessibilityInteractor,
    deviceEntryIconViewModel: DeviceEntryIconViewModel,
    deviceEntryFgIconViewModel: DeviceEntryForegroundViewModel,
) {
    private val udfpsUtils = UdfpsUtils()
    private val udfpsOverlayParams: StateFlow<UdfpsOverlayParams> =
        udfpsOverlayInteractor.udfpsOverlayParams

    /** Overlay is only visible if touch exploration is enabled and UDFPS can be used. */
    val visible: Flow<Boolean> =
        accessibilityInteractor.isTouchExplorationEnabled.flatMapLatest { touchExplorationEnabled ->
            if (touchExplorationEnabled) {
                combine(
                    deviceEntryFgIconViewModel.viewModel,
                    deviceEntryIconViewModel.deviceEntryViewAlpha,
                ) { iconViewModel, alpha ->
                    iconViewModel.type == DeviceEntryIconView.IconType.FINGERPRINT &&
                        !iconViewModel.useAodVariant &&
                        alpha == 1f
                }
            } else {
                flowOf(false)
            }
        }

    /** Give directional feedback to help the user authenticate with UDFPS. */
    fun onHoverEvent(v: View, event: MotionEvent): Boolean {
        val overlayParams = udfpsOverlayParams.value
        val scaledTouch: Point =
            udfpsUtils.getTouchInNativeCoordinates(event.getPointerId(0), event, overlayParams)

        if (!udfpsUtils.isWithinSensorArea(event.getPointerId(0), event, overlayParams)) {
            // view only receives motionEvents when [visible] which requires touchExplorationEnabled
            val announceStr =
                udfpsUtils.onTouchOutsideOfSensorArea(
                    /* touchExplorationEnabled */ true,
                    v.context,
                    scaledTouch.x,
                    scaledTouch.y,
                    overlayParams,
                )
            if (announceStr != null) {
                v.announceForAccessibility(announceStr)
            }
        }
        // always let the motion events go through to underlying views
        return false
    }
}
+6 −1
Original line number Diff line number Diff line
@@ -31,18 +31,21 @@ import com.android.systemui.keyguard.ui.view.layout.sections.DefaultSettingsPopu
import com.android.systemui.keyguard.ui.view.layout.sections.DefaultShortcutsSection
import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusBarSection
import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusViewSection
import com.android.systemui.keyguard.ui.view.layout.sections.DefaultUdfpsAccessibilityOverlaySection
import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSectionsModule.Companion.KEYGUARD_AMBIENT_INDICATION_AREA_SECTION
import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection
import java.util.Optional
import javax.inject.Inject
import javax.inject.Named
import kotlin.jvm.optionals.getOrNull
import kotlinx.coroutines.ExperimentalCoroutinesApi

/**
 * Positions elements of the lockscreen to the default position.
 *
 * This will be the most common use case for phones in portrait mode.
 */
@ExperimentalCoroutinesApi
@SysUISingleton
@JvmSuppressWildcards
class DefaultKeyguardBlueprint
@@ -62,6 +65,7 @@ constructor(
    communalTutorialIndicatorSection: CommunalTutorialIndicatorSection,
    clockSection: ClockSection,
    smartspaceSection: SmartspaceSection,
    udfpsAccessibilityOverlaySection: DefaultUdfpsAccessibilityOverlaySection,
) : KeyguardBlueprint {
    override val id: String = DEFAULT

@@ -79,7 +83,8 @@ constructor(
            aodBurnInSection,
            communalTutorialIndicatorSection,
            clockSection,
            defaultDeviceEntrySection, // Add LAST: Intentionally has z-order above other views.
            defaultDeviceEntrySection,
            udfpsAccessibilityOverlaySection, // Add LAST: Intentionally has z-order above others
        )

    companion object {
Loading