1 package com.android.systemui.biometrics.ui.binder
2 
3 import android.os.UserHandle
4 import android.view.KeyEvent
5 import android.view.View
6 import android.view.inputmethod.EditorInfo
7 import android.view.inputmethod.InputMethodManager
8 import android.widget.ImeAwareEditText
9 import android.widget.TextView
10 import android.window.OnBackInvokedCallback
11 import android.window.OnBackInvokedDispatcher
12 import androidx.lifecycle.Lifecycle
13 import androidx.lifecycle.lifecycleScope
14 import androidx.lifecycle.repeatOnLifecycle
15 import com.android.systemui.R
16 import com.android.systemui.biometrics.ui.CredentialPasswordView
17 import com.android.systemui.biometrics.ui.CredentialView
18 import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel
19 import com.android.systemui.lifecycle.repeatWhenAttached
20 import kotlinx.coroutines.awaitCancellation
21 import kotlinx.coroutines.flow.first
22 import kotlinx.coroutines.flow.firstOrNull
23 import kotlinx.coroutines.launch
24 
25 /** Sub-binder for the [CredentialPasswordView]. */
26 object CredentialPasswordViewBinder {
27 
28     /** Bind the view. */
29     fun bind(
30         view: CredentialPasswordView,
31         host: CredentialView.Host,
32         viewModel: CredentialViewModel,
33         requestFocusForInput: Boolean,
34     ) {
35         val imeManager = view.context.getSystemService(InputMethodManager::class.java)!!
36 
37         val passwordField: ImeAwareEditText = view.requireViewById(R.id.lockPassword)
38 
39         val onBackInvokedCallback = OnBackInvokedCallback { host.onCredentialAborted() }
40 
41         view.repeatWhenAttached {
42             // the header info never changes - do it early
43             val header = viewModel.header.first()
44             passwordField.setTextOperationUser(UserHandle.of(header.user.deviceCredentialOwnerId))
45             viewModel.inputFlags.firstOrNull()?.let { flags -> passwordField.inputType = flags }
46             if (requestFocusForInput) {
47                 passwordField.requestFocus()
48                 passwordField.scheduleShowSoftInput()
49             }
50             passwordField.setOnEditorActionListener(
51                 OnImeSubmitListener { text ->
52                     lifecycleScope.launch { viewModel.checkCredential(text, header) }
53                 }
54             )
55             passwordField.setOnKeyListener(OnBackButtonListener(onBackInvokedCallback))
56 
57             repeatOnLifecycle(Lifecycle.State.STARTED) {
58                 // dismiss on a valid credential check
59                 launch {
60                     viewModel.validatedAttestation.collect { attestation ->
61                         if (attestation != null) {
62                             imeManager.hideSoftInputFromWindow(view.windowToken, 0 /* flags */)
63                             host.onCredentialMatched(attestation)
64                         } else {
65                             passwordField.setText("")
66                         }
67                     }
68                 }
69 
70                 val onBackInvokedDispatcher = view.findOnBackInvokedDispatcher()
71                 if (onBackInvokedDispatcher != null) {
72                     launch {
73                             onBackInvokedDispatcher.registerOnBackInvokedCallback(
74                                 OnBackInvokedDispatcher.PRIORITY_DEFAULT,
75                                 onBackInvokedCallback
76                             )
77                             awaitCancellation()
78                         }
79                         .invokeOnCompletion {
80                             onBackInvokedDispatcher.unregisterOnBackInvokedCallback(
81                                 onBackInvokedCallback
82                             )
83                         }
84                 }
85             }
86         }
87     }
88 }
89 
90 private class OnBackButtonListener(private val onBackInvokedCallback: OnBackInvokedCallback) :
91     View.OnKeyListener {
92     override fun onKey(v: View, keyCode: Int, event: KeyEvent): Boolean {
93         if (keyCode != KeyEvent.KEYCODE_BACK) {
94             return false
95         }
96         if (event.action == KeyEvent.ACTION_UP) {
97             onBackInvokedCallback.onBackInvoked()
98         }
99         return true
100     }
101 }
102 
103 private class OnImeSubmitListener(private val onSubmit: (text: CharSequence) -> Unit) :
104     TextView.OnEditorActionListener {
105     override fun onEditorAction(v: TextView, actionId: Int, event: KeyEvent?): Boolean {
106         val isSoftImeEvent =
107             event == null &&
108                 (actionId == EditorInfo.IME_NULL ||
109                     actionId == EditorInfo.IME_ACTION_DONE ||
110                     actionId == EditorInfo.IME_ACTION_NEXT)
111         val isKeyboardEnterKey =
112             event != null &&
113                 KeyEvent.isConfirmKey(event.keyCode) &&
114                 event.action == KeyEvent.ACTION_DOWN
115         if (isSoftImeEvent || isKeyboardEnterKey) {
116             onSubmit(v.text)
117             return true
118         }
119         return false
120     }
121 }
122