/* * 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.keyguard.ui.binder import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.WindowManager import android.window.OnBackInvokedCallback import android.window.OnBackInvokedDispatcher import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.ui.binder.UdfpsAccessibilityOverlayBinder import com.android.systemui.deviceentry.ui.view.UdfpsAccessibilityOverlay import com.android.systemui.deviceentry.ui.viewmodel.AlternateBouncerUdfpsAccessibilityOverlayViewModel import com.android.systemui.keyguard.ui.view.AlternateBouncerWindowViewLayoutParams import com.android.systemui.keyguard.ui.view.DeviceEntryIconView import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerUdfpsIconViewModel import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerWindowViewModel import com.android.systemui.lifecycle.WindowLifecycleState import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.lifecycle.viewModel import com.android.systemui.log.TouchHandlingViewLogger import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scrim.ScrimView import dagger.Lazy import javax.inject.Inject import kotlinx.coroutines.CoroutineScope /** * When necessary, adds the alternate bouncer window above most other windows (including the * notification shade, system UI dialogs) but below the UDFPS touch overlay and SideFPS indicator. * Also binds the alternate bouncer view to its view-model. * * For devices that support UDFPS, this view includes a UDFPS view. */ @SysUISingleton class AlternateBouncerViewBinder @Inject constructor( @Application private val applicationScope: CoroutineScope, private val alternateBouncerWindowViewModel: Lazy, private val alternateBouncerDependencies: Lazy, private val windowManager: Lazy, private val layoutInflater: Lazy, ) : CoreStartable { private var alternateBouncerView: ConstraintLayout? = null override fun start() { if (SceneContainerFlag.isEnabled) { return } applicationScope.launch("$TAG#alternateBouncerWindowViewModel") { alternateBouncerWindowViewModel.get().alternateBouncerWindowRequired.collect { addAlternateBouncerWindowView -> Log.d(TAG, "alternateBouncerWindowRequired=$addAlternateBouncerWindowView") if (addAlternateBouncerWindowView) { addViewToWindowManager() val scrim: ScrimView = alternateBouncerView!!.requireViewById(R.id.alternate_bouncer_scrim) scrim.viewAlpha = 0f bind(alternateBouncerView!!, alternateBouncerDependencies.get()) } else { removeViewFromWindowManager() alternateBouncerDependencies.get().viewModel.onRemovedFromWindow() } } } } private fun removeViewFromWindowManager() { alternateBouncerView?.let { alternateBouncerView = null if (it.isAttachedToWindow) { it.removeOnAttachStateChangeListener(onAttachAddBackGestureHandler) Log.d(TAG, "Removing alternate bouncer view immediately") windowManager.get().removeView(it) } else { // once the view is attached, remove it it.addOnAttachStateChangeListener( object : View.OnAttachStateChangeListener { override fun onViewAttachedToWindow(view: View) { it.removeOnAttachStateChangeListener(this) it.removeOnAttachStateChangeListener(onAttachAddBackGestureHandler) Log.d(TAG, "Removing alternate bouncer view on attached") windowManager.get().removeView(it) } override fun onViewDetachedFromWindow(view: View) {} } ) } } } private val onAttachAddBackGestureHandler = object : View.OnAttachStateChangeListener { private val onBackInvokedCallback: OnBackInvokedCallback = OnBackInvokedCallback { alternateBouncerDependencies.get().viewModel.onBackRequested() } override fun onViewAttachedToWindow(view: View) { view .findOnBackInvokedDispatcher() ?.registerOnBackInvokedCallback( OnBackInvokedDispatcher.PRIORITY_OVERLAY, onBackInvokedCallback, ) } override fun onViewDetachedFromWindow(view: View) { view .findOnBackInvokedDispatcher() ?.unregisterOnBackInvokedCallback(onBackInvokedCallback) } } private fun addViewToWindowManager() { if (SceneContainerFlag.isEnabled) { return } if (alternateBouncerView != null) { return } alternateBouncerView = layoutInflater.get().inflate(R.layout.alternate_bouncer, null, false) as ConstraintLayout Log.d(TAG, "Adding alternate bouncer view") windowManager .get() .addView(alternateBouncerView, AlternateBouncerWindowViewLayoutParams.layoutParams) alternateBouncerView!!.addOnAttachStateChangeListener(onAttachAddBackGestureHandler) } /** Binds the view to the view-model, continuing to update the former based on the latter. */ fun bind(view: ConstraintLayout, alternateBouncerDependencies: AlternateBouncerDependencies) { optionallyAddUdfpsViews( view = view, logger = alternateBouncerDependencies.logger, udfpsIconViewModel = alternateBouncerDependencies.udfpsIconViewModel, udfpsA11yOverlayViewModel = alternateBouncerDependencies.udfpsAccessibilityOverlayViewModel, ) AlternateBouncerMessageAreaViewBinder.bind( view = view.requireViewById(R.id.alternate_bouncer_message_area), viewModel = alternateBouncerDependencies.messageAreaViewModel, ) val scrim: ScrimView = view.requireViewById(R.id.alternate_bouncer_scrim) val viewModel = alternateBouncerDependencies.viewModel val swipeUpAnywhereGestureHandler = alternateBouncerDependencies.swipeUpAnywhereGestureHandler val tapGestureDetector = alternateBouncerDependencies.tapGestureDetector view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { launch("$TAG#viewModel.registerForDismissGestures") { viewModel.registerForDismissGestures.collect { registerForDismissGestures -> if (registerForDismissGestures) { swipeUpAnywhereGestureHandler.addOnGestureDetectedCallback( swipeTag ) { _ -> alternateBouncerDependencies.powerInteractor.onUserTouch() viewModel.onTapped() } tapGestureDetector.addOnGestureDetectedCallback(tapTag) { _ -> alternateBouncerDependencies.powerInteractor.onUserTouch() viewModel.onTapped() } } else { swipeUpAnywhereGestureHandler.removeOnGestureDetectedCallback( swipeTag ) tapGestureDetector.removeOnGestureDetectedCallback(tapTag) } } } .invokeOnCompletion { swipeUpAnywhereGestureHandler.removeOnGestureDetectedCallback(swipeTag) tapGestureDetector.removeOnGestureDetectedCallback(tapTag) } launch("$TAG#viewModel.scrimAlpha") { viewModel.scrimAlpha.collect { scrim.viewAlpha = it } } launch("$TAG#viewModel.scrimColor") { viewModel.scrimColor.collect { scrim.tint = it } } } } view.repeatWhenAttached { view.viewModel( traceName = "AlternateBouncerViewBinderViewModel", minWindowLifecycleState = WindowLifecycleState.ATTACHED, factory = { alternateBouncerDependencies.viewModel }, ) { // no-op - currently used to activate viewModel } } } private fun optionallyAddUdfpsViews( view: ConstraintLayout, logger: TouchHandlingViewLogger, udfpsIconViewModel: AlternateBouncerUdfpsIconViewModel, udfpsA11yOverlayViewModel: Lazy, ) { view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.CREATED) { launch("$TAG#udfpsIconViewModel.iconLocation") { udfpsIconViewModel.iconLocation.collect { iconLocation -> // add UDFPS a11y overlay val udfpsA11yOverlayViewId = R.id.alternate_bouncer_udfps_accessibility_overlay var udfpsA11yOverlay = view.getViewById(udfpsA11yOverlayViewId) if (udfpsA11yOverlay == null) { udfpsA11yOverlay = UdfpsAccessibilityOverlay(view.context).apply { id = udfpsA11yOverlayViewId } view.addView(udfpsA11yOverlay) UdfpsAccessibilityOverlayBinder.bind( udfpsA11yOverlay, udfpsA11yOverlayViewModel.get(), ) } // add UDFPS icon view val udfpsViewId = R.id.alternate_bouncer_udfps_icon_view var udfpsView = view.getViewById(udfpsViewId) if (udfpsView == null) { udfpsView = DeviceEntryIconView(view.context, null, logger = logger).apply { id = udfpsViewId contentDescription = context.resources.getString( R.string.accessibility_fingerprint_label ) } view.addView(udfpsView) AlternateBouncerUdfpsViewBinder.bind(udfpsView, udfpsIconViewModel) } val constraintSet = ConstraintSet().apply { clone(view) } constraintSet.apply { // udfpsView: constrainWidth(udfpsViewId, iconLocation.width) constrainHeight(udfpsViewId, iconLocation.height) connect( udfpsViewId, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP, iconLocation.top, ) connect( udfpsViewId, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START, iconLocation.left, ) // udfpsA11yOverlayView: constrainWidth( udfpsA11yOverlayViewId, ViewGroup.LayoutParams.MATCH_PARENT, ) constrainHeight( udfpsA11yOverlayViewId, ViewGroup.LayoutParams.MATCH_PARENT, ) } constraintSet.applyTo(view) } } } } } companion object { private const val TAG = "AlternateBouncerViewBinder" private const val swipeTag = "AlternateBouncer-SWIPE" private const val tapTag = "AlternateBouncer-TAP" } }