· 7 years ago · Dec 02, 2018, 09:38 PM
1class FingerprintDialog : DialogFragment(), FingerprintController.Callback {
2
3private val controller: FingerprintController by lazy {
4 FingerprintController(
5 FingerprintManagerCompat.from(this.context!!),
6 this,
7 titleTextView,
8 subtitleTextView,
9 errorTextView,
10 iconFAB
11 )
12}
13
14
15private var keyStore: KeyStore? = null
16
17
18private var keyGenerator: KeyGenerator? = null
19
20override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
21 inflater.inflate(R.layout.dialog_fingerprint, container, false)
22
23
24override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
25 super.onViewCreated(view!!, savedInstanceState)
26
27 controller.setTitle(arguments!!.getString(ARG_TITLE))
28 controller.setSubtitle(arguments!!.getString(ARG_SUBTITLE))
29}
30
31@RequiresApi(Build.VERSION_CODES.M)
32override fun onCreate(savedInstanceState: Bundle?) {
33 super.onCreate(savedInstanceState)
34
35 try {
36 keyStore = KeyStore.getInstance("AndroidKeyStore")
37 } catch (e: KeyStoreException) {
38 throw RuntimeException("Failed to get an instance of KeyStore", e)
39 }
40
41 try {
42 keyGenerator = KeyGenerator
43 .getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
44 } catch (e: NoSuchAlgorithmException) {
45 throw RuntimeException("Failed to get an instance of KeyGenerator", e)
46 } catch (e: NoSuchProviderException) {
47 throw RuntimeException("Failed to get an instance of KeyGenerator", e)
48 }
49
50 createKey(DEFAULT_KEY_NAME, false)
51
52 val defaultCipher: Cipher
53 try {
54 defaultCipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
55 + KeyProperties.BLOCK_MODE_CBC + "/"
56 + KeyProperties.ENCRYPTION_PADDING_PKCS7)
57 } catch (e: NoSuchAlgorithmException) {
58 throw RuntimeException("Failed to get an instance of Cipher", e)
59 } catch (e: NoSuchPaddingException) {
60 throw RuntimeException("Failed to get an instance of Cipher", e)
61 }
62
63 if (initCipher(defaultCipher, DEFAULT_KEY_NAME)) {
64 cryptoObject = FingerprintManagerCompat.CryptoObject(defaultCipher)
65 }
66}
67
68override fun onResume() {
69 super.onResume()
70
71 dialog?.window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
72 cryptoObject?.let {
73 controller.startListening(it)
74 }
75}
76
77override fun onPause() {
78 super.onPause()
79 controller.stopListening()
80}
81
82override fun onAuthenticated() {
83 //TODO:
84}
85
86override fun onError() {
87 //TODO:
88}
89
90
91@RequiresApi(Build.VERSION_CODES.M)
92private fun initCipher(cipher: Cipher, keyName: String): Boolean {
93 try {
94 keyStore?.load(null)
95 val key = keyStore?.getKey(keyName, null) as SecretKey
96 cipher.init(Cipher.ENCRYPT_MODE, key)
97 return true
98 } catch (e: KeyPermanentlyInvalidatedException) {
99 return false
100 } catch (e: KeyStoreException) {
101 throw RuntimeException("Failed to init Cipher", e)
102 } catch (e: CertificateException) {
103 throw RuntimeException("Failed to init Cipher", e)
104 } catch (e: UnrecoverableKeyException) {
105 throw RuntimeException("Failed to init Cipher", e)
106 } catch (e: IOException) {
107 throw RuntimeException("Failed to init Cipher", e)
108 } catch (e: NoSuchAlgorithmException) {
109 throw RuntimeException("Failed to init Cipher", e)
110 } catch (e: InvalidKeyException) {
111 throw RuntimeException("Failed to init Cipher", e)
112 }
113}
114
115
116@RequiresApi(Build.VERSION_CODES.M)
117private fun createKey(keyName: String, invalidatedByBiometricEnrollment: Boolean) {
118 to set up fingerprint
119 set of
120 // enrolled fingerprints has changed.
121 try {
122 keyStore?.load(null)
123 // Set the alias of the entry in Android KeyStore where the key will appear
124 // and the constrains (purposes) in the constructor of the Builder
125
126 val builder = KeyGenParameterSpec.Builder(keyName,
127 KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
128 .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
129 // Require the user to authenticate with a fingerprint to authorize every use
130 // of the key
131 .setUserAuthenticationRequired(true)
132 .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
133
134 // This is a workaround to avoid crashes on devices whose API level is < 24
135 // because KeyGenParameterSpec.Builder#setInvalidatedByBiometricEnrollment is only
136 // visible on API level +24.
137 // Ideally there should be a compat library for KeyGenParameterSpec.Builder but
138 // which isn't available yet.
139 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
140 builder.setInvalidatedByBiometricEnrollment(invalidatedByBiometricEnrollment)
141 }
142 keyGenerator?.init(builder.build())
143 keyGenerator?.generateKey()
144 } catch (e: NoSuchAlgorithmException) {
145 throw RuntimeException(e)
146 } catch (e: InvalidAlgorithmParameterException) {
147 throw RuntimeException(e)
148 } catch (e: CertificateException) {
149 throw RuntimeException(e)
150 } catch (e: IOException) {
151 throw RuntimeException(e)
152 }
153
154}
155
156companion object {
157
158 fun newInstance(title: String, subtitle: String): FingerprintDialog {
159 val args = Bundle()
160 args.putString(ARG_TITLE, title)
161 args.putString(ARG_SUBTITLE, subtitle)
162
163 val fragment = FingerprintDialog()
164 fragment.arguments = args
165
166 return fragment
167 }
168}
169
170class FingerprintController(private val fingerprintManager: FingerprintManagerCompat, private val callback: Callback, private val title: TextView, private val subtitle: TextView, private val errorText: TextView, private val icon: ImageView) : FingerprintManagerCompat.AuthenticationCallback() {
171
172
173private var cancellationSignal: CancellationSignal? = null
174
175private var selfCancelled = false
176
177
178private val isFingerprintAuthAvailable: Boolean
179 get() = fingerprintManager.isHardwareDetected && fingerprintManager.hasEnrolledFingerprints()
180
181
182private val context: Context
183 get() = errorText.context
184
185
186private val resetErrorTextRunnable: Runnable = Runnable {
187 errorText.setTextColor(ContextCompat.getColor(context, R.color.hint_color))
188 errorText.text = context.getString(R.string.touch_sensor)
189 icon.setImageResource(R.drawable.ic_fingerprint_white_24dp)
190}
191
192init {
193 errorText.post(resetErrorTextRunnable)
194}
195
196
197fun startListening(cryptoObject: FingerprintManagerCompat.CryptoObject) {
198 if (!isFingerprintAuthAvailable) return
199
200 cancellationSignal = CancellationSignal()
201 selfCancelled = false
202 fingerprintManager.authenticate(cryptoObject, 0, cancellationSignal, this, null)
203}
204
205
206fun stopListening() {
207 cancellationSignal?.let {
208 selfCancelled = true
209 it.cancel()
210 cancellationSignal = null
211 }
212}
213
214
215private fun showError(text: CharSequence?) {
216 icon.setImageResource(R.drawable.ic_error_white_24dp)
217 errorText.text = text
218 errorText.setTextColor(ContextCompat.getColor(errorText.context, R.color.warning_color))
219 errorText.removeCallbacks(resetErrorTextRunnable)
220 errorText.postDelayed(resetErrorTextRunnable, ERROR_TIMEOUT_MILLIS)
221}
222
223override fun onAuthenticationError(errMsgId: Int, errString: CharSequence?) {
224 if (!selfCancelled) {
225 showError(errString)
226 icon.postDelayed({
227 callback.onError()
228 }, ERROR_TIMEOUT_MILLIS)
229 }
230}
231
232@RequiresApi(Build.VERSION_CODES.O)
233override fun onAuthenticationSucceeded(result: FingerprintManagerCompat.AuthenticationResult?) {
234 errorText.removeCallbacks(resetErrorTextRunnable)
235 icon.setImageResource(R.drawable.ic_check_white_24dp)
236 errorText.setTextColor(ContextCompat.getColor(errorText.context, R.color.success_color))
237 errorText.text = errorText.context.getString(R.string.fingerprint_recognized)
238 icon.postDelayed({
239 callback.onAuthenticated()
240 }, SUCCESS_DELAY_MILLIS)
241}
242
243override fun onAuthenticationHelp(helpMsgId: Int, helpString: CharSequence?) {
244 showError(helpString)
245}
246
247override fun onAuthenticationFailed() {
248 showError(errorText.context.getString(R.string.fingerprint_not_recognized))
249}
250
251
252fun setTitle(title: CharSequence) {
253 this.title.text = title
254}
255
256
257fun setSubtitle(subtitle: CharSequence) {
258 this.subtitle.text = subtitle
259}
260
261companion object {
262
263 private val ERROR_TIMEOUT_MILLIS = 1600L
264
265
266 private val SUCCESS_DELAY_MILLIS = 1300L
267}
268
269
270interface Callback {
271
272 fun onAuthenticated()
273
274
275 fun onError()
276}